Skip to content

Commit

Permalink
Part of #3928 [Web Examples] Add First Class Python Support (#4107)
Browse files Browse the repository at this point in the history
# Pull Request 
Adds First Class Python Support [Web Examples]
Part of #3928

## Description
Web Examples for Python `1-hello-flask`, `2-todo-flask`,
`3-hello-django`, `4-todo-django` Examples

## Related Issues
- Link to related issue #3928.

## Checklist

- [x]  1-hello-flask
- [x]  2-todo-flask
- [x]  3-hello-django
- [x]  4-todo-django
- [x] Updated Documentation

## Status
Require Review!!!
  • Loading branch information
himanshumahajan138 authored Dec 23, 2024
1 parent 7cea88c commit d361ace
Show file tree
Hide file tree
Showing 47 changed files with 2,202 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
*** xref:pythonlib/module-config.adoc[]
*** xref:pythonlib/dependencies.adoc[]
*** xref:pythonlib/publishing.adoc[]
*** xref:pythonlib/web-examples.adoc[]
* xref:comparisons/why-mill.adoc[]
** xref:comparisons/maven.adoc[]
** xref:comparisons/gradle.adoc[]
Expand Down
25 changes: 25 additions & 0 deletions docs/modules/ROOT/pages/pythonlib/web-examples.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
= Python Web Project Examples
:page-aliases: Python_Web_Examples.adoc

include::partial$gtag-config.adoc[]

This page provides examples of using Mill as a build tool for Python web applications.
It includes setting up a basic "Hello, World!" application and developing a fully
functional https://todomvc.com/[TodoMVC] app with Flask and Django, showcasing best practices
for project organization, scalability, and maintainability.

== Flask Hello World App

include::partial$example/pythonlib/web/1-hello-flask.adoc[]

== Flask TodoMVC App

include::partial$example/pythonlib/web/2-todo-flask.adoc[]

== Django Hello World App

include::partial$example/pythonlib/web/3-hello-django.adoc[]

== Django TodoMVC App

include::partial$example/pythonlib/web/4-todo-django.adoc[]
1 change: 1 addition & 0 deletions example/package.mill
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ object `package` extends RootModule with Module {
object dependencies extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "dependencies"))
object publishing extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "publishing"))
object module extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "module"))
object web extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "web"))
}

object cli extends Module{
Expand Down
39 changes: 39 additions & 0 deletions example/pythonlib/web/1-hello-flask/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// This example uses Mill to manage a Flask app that serves `"Hello, Mill!"`
// at the root URL (`/`), with Flask installed as a dependency
// and tests enabled using `unittest`.
package build
import mill._, pythonlib._

object foo extends PythonModule {

def mainScript = Task.Source { millSourcePath / "src/foo.py" }

def pythonDeps = Seq("flask==3.1.0")

object test extends PythonTests with TestModule.Unittest

}

// Running these commands will test and run the Flask server with desired outputs.

// The app is ready to serve at `http://localhost:5000`.

/** Usage

> ./mill foo.test
...
test_hello_flask (test.TestScript...)
Test the '/' endpoint. ... ok
...
Ran 1 test...
OK
...

> ./mill foo.runBackground

> curl http://localhost:5000
...<h1>Hello, Mill!</h1>...

> ./mill clean foo.runBackground

*/
12 changes: 12 additions & 0 deletions example/pythonlib/web/1-hello-flask/foo/src/foo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from flask import Flask

app = Flask(__name__)


@app.route("/")
def hello_world():
return "<h1>Hello, Mill!</h1>"


if __name__ == "__main__":
app.run(debug=True)
21 changes: 21 additions & 0 deletions example/pythonlib/web/1-hello-flask/foo/test/src/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import unittest
from foo import app # type: ignore


class TestScript(unittest.TestCase):
def setUp(self):
"""Set up the test client before each test."""
self.app = app.test_client() # Initialize the test client
self.app.testing = True # Enable testing mode for better error handling

def test_hello_flask(self):
"""Test the '/' endpoint."""
response = self.app.get("/") # Simulate a GET request to the root endpoint
self.assertEqual(response.status_code, 200) # Check the HTTP status code
self.assertIn(
b"Hello, Mill!", response.data
) # Check if the response contains the expected text


if __name__ == "__main__":
unittest.main()
64 changes: 64 additions & 0 deletions example/pythonlib/web/2-todo-flask/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// This is a `Flask`-based https://todomvc.com/[TodoMVC] application managed and built using `Mill`.
// It allows users to `add`, `edit`, `delete`, and `view` tasks stored in a Python Data Structure.
// Tasks can be filtered as `all`, `active`, or `completed` based on their state.
// The application demonstrates dynamic rendering using Flask's routing and templating features.
package build
import mill._, pythonlib._

object todo extends PythonModule {

def mainScript = Task.Source { millSourcePath / "src/app.py" }

def pythonDeps = Seq("flask==3.1.0", "Flask-SQLAlchemy==3.1.1", "Flask-WTF==1.2.2")

object test extends PythonTests with TestModule.Unittest
object itest extends PythonTests with TestModule.Unittest

}

// Apart from running a web server, this example demonstrates:

// - **Serving HTML templates** using **Jinja2** (Flask's default templating engine).
// - **Managing static files** such as JavaScript, CSS, and images.
// - **Filtering and managing tasks** in-memory using Python data structures.
// - **Unit testing** using **unittest** for testing task operations.
// - **Integration testing** using **unittest** for end-to-end application behavior.

// This example also utilizes **Mill** for managing `dependencies`, `builds`, and `tests`,
// offering an efficient development workflow.

// The app is ready to serve at `http://localhost:5001`.

/** Usage

> ./mill todo.test
...
test_add_todo (test.TestTodoApp...) ... ok
test_delete_todo (test.TestTodoApp...) ... ok
test_edit_todo (test.TestTodoApp...) ... ok
test_filter_todos (test.TestTodoApp...) ... ok
test_toggle_all (test.TestTodoApp...) ... ok
test_toggle_todo (test.TestTodoApp...) ... ok
...Ran 6 tests...
OK
...

> ./mill todo.itest
...
test_add_and_list_todos (test.TestTodoAppIntegration...) ... ok
test_delete_todo (test.TestTodoAppIntegration...) ... ok
test_edit_and_list_todos (test.TestTodoAppIntegration...) ... ok
test_toggle_all_todos (test.TestTodoAppIntegration...) ... ok
test_toggle_and_list_todos (test.TestTodoAppIntegration...) ... ok
...Ran 5 tests...
OK
...

> ./mill todo.runBackground

> curl http://localhost:5001
...What needs to be done...

> ./mill clean todo.runBackground

*/
68 changes: 68 additions & 0 deletions example/pythonlib/web/2-todo-flask/todo/itest/src/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import unittest
from app import app, todos


class TestTodoAppIntegration(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Set up the test client for the app
cls.client = app.test_client()

def setUp(self):
# Clear the todos list before each test
global todos
todos.clear()

def test_add_and_list_todos(self):
# Test adding a todo and listing all todos
response = self.client.post("/add/all", data="Test Todo")
self.assertEqual(response.status_code, 200)
# Fetch the todos list and verify the item was added
response = self.client.post("/list/all", data="")
self.assertIn(b"Test Todo", response.data)

def test_toggle_and_list_todos(self):
# Test adding a todo, toggling it, and listing active/completed todos
self.client.post("/add/all", data="Test Todo")
response = self.client.post("/toggle/all/0", data="")
# Check if the todo is toggled
self.assertEqual(response.status_code, 200)
# Now, test filtering todos based on active/completed state
response = self.client.post("/list/active", data="")
self.assertNotIn(b"Test Todo", response.data)
response = self.client.post("/list/completed", data="")
self.assertIn(b"Test Todo", response.data)

def test_edit_and_list_todos(self):
# Test adding a todo, editing it, and then verifying the updated text
self.client.post("/add/all", data="Test Todo")
response = self.client.post("/edit/all/0", data="Updated Todo")
# Check that the todo was updated
response = self.client.post("/list/all", data="")
self.assertIn(b"Updated Todo", response.data)
self.assertNotIn(b"Test Todo", response.data)

def test_delete_todo(self):
# Test adding and deleting a todo
self.client.post("/add/all", data="Test Todo")
response = self.client.post("/delete/all/0", data="")
# Verify that the todo was deleted
response = self.client.post("/list/all", data="")
self.assertNotIn(b"Test Todo", response.data)

def test_toggle_all_todos(self):
# Test toggling all todos
self.client.post("/add/all", data="Todo 1")
self.client.post("/add/all", data="Todo 2")
response = self.client.post("/toggle-all/all", data="")
response = self.client.post("/list/completed", data="")
self.assertIn(b"Todo 1", response.data)
self.assertIn(b"Todo 2", response.data)
response = self.client.post("/toggle-all/all", data="")
response = self.client.post("/list/active", data="")
self.assertIn(b"Todo 1", response.data)
self.assertIn(b"Todo 2", response.data)


if __name__ == "__main__":
unittest.main()
96 changes: 96 additions & 0 deletions example/pythonlib/web/2-todo-flask/todo/src/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from flask import Flask, render_template, request
from dataclasses import dataclass
from typing import List

app = Flask(__name__, static_folder="../static", template_folder="../templates")


@dataclass
class Todo:
checked: bool
text: str


todos: List[Todo] = []


@app.route("/")
def index():
return render_template("base.html", todos=todos, state="all")


def render_body(state: str):
filtered_todos = {
"all": todos,
"active": [todo for todo in todos if not todo.checked],
"completed": [todo for todo in todos if todo.checked],
}[state]
return render_template("index.html", todos=filtered_todos, state=state)


def filter_todos(state):
"""Filter todos based on the state (all, active, completed)."""
if state == "all":
return todos
elif state == "active":
return [todo for todo in todos if not todo.checked]
elif state == "completed":
return [todo for todo in todos if todo.checked]


@app.route("/edit/<state>/<int:index>", methods=["POST"])
def edit_todo(state, index):
"""Edit the text of a todo."""
global todos
updated_text = request.data.decode("utf-8")
# Update the text attribute of the Todo object
todos[index].text = updated_text
filtered_todos = filter_todos(state)
return render_template("index.html", todos=filtered_todos, state=state)


@app.route("/list/<state>", methods=["POST"])
def list_todos(state):
return render_body(state)


@app.route("/add/<state>", methods=["POST"])
def add_todo(state):
todos.insert(0, Todo(checked=False, text=request.data.decode("utf-8")))
return render_body(state)


@app.route("/delete/<state>/<int:index>", methods=["POST"])
def delete_todo(state, index):
if 0 <= index < len(todos):
todos.pop(index)
return render_body(state)


@app.route("/toggle/<state>/<int:index>", methods=["POST"])
def toggle(state, index):
if 0 <= index < len(todos):
todos[index].checked = not todos[index].checked
return render_body(state)


@app.route("/clear-completed/<state>", methods=["POST"])
def clear_completed(state):
global todos
todos = [todo for todo in todos if not todo.checked]
return render_body(state)


@app.route("/toggle-all/<state>", methods=["POST"])
def toggle_all(state):
global todos

all_checked = all(todo.checked for todo in todos)
for todo in todos:
todo.checked = not all_checked

return render_body(state)


if __name__ == "__main__":
app.run(debug=True, port=5001)
Loading

0 comments on commit d361ace

Please sign in to comment.