Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Fixes: #3928 [Web Examples] Add First Class Python Support #4088

Closed
1 change: 1 addition & 0 deletions example/package.mill
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ object `package` extends RootModule with Module {
object basic extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "basic"))
object dependencies extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "dependencies"))
object publishing extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "publishing"))
object web extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "web"))
object module extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "module"))
}

Expand Down
32 changes: 32 additions & 0 deletions example/pythonlib/web/1-hello-flask/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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

}

/** 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

*/
10 changes: 10 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,10 @@
from flask import Flask

app = Flask(__name__)

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

if __name__ == '__main__':
app.run(debug=True)
17 changes: 17 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,17 @@
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()
24 changes: 24 additions & 0 deletions example/pythonlib/web/2-todo-flask/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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

}

// TODO: Testing will be added soon...

/** Usage

> ./mill todo.runBackground

> curl http://localhost:5001

> ./mill clean todo.runBackground

*/
74 changes: 74 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,74 @@
from flask import Flask, render_template, redirect, url_for, flash, request
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SelectField, DateField, SubmitField
from wtforms.validators import DataRequired, Length

# Initialize Flask App and Database
app = Flask(__name__, static_folder="../static", template_folder="../templates")
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///todo.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SECRET_KEY"] = "your_secret_key"

# Import models
from models import Task, db

# Import forms
from forms import TaskForm

db.init_app(app)


# Routes
@app.route("/")
def index():
tasks = Task.query.all()
return render_template("index.html", tasks=tasks)


@app.route("/add", methods=["GET", "POST"])
def add_task():
form = TaskForm()
if form.validate_on_submit():
new_task = Task(
title=form.title.data,
description=form.description.data,
status=form.status.data,
deadline=form.deadline.data,
)
db.session.add(new_task)
db.session.commit()
flash("Task added successfully!", "success")
return redirect(url_for("index"))
return render_template("task.html", form=form, title="Add Task")


@app.route("/edit/<int:task_id>", methods=["GET", "POST"])
def edit_task(task_id):
task = Task.query.get_or_404(task_id)
form = TaskForm(obj=task)
if form.validate_on_submit():
task.title = form.title.data
task.description = form.description.data
task.status = form.status.data
task.deadline = form.deadline.data
db.session.commit()
flash("Task updated successfully!", "success")
return redirect(url_for("index"))
return render_template("task.html", form=form, title="Edit Task")


@app.route("/delete/<int:task_id>")
def delete_task(task_id):
task = Task.query.get_or_404(task_id)
db.session.delete(task)
db.session.commit()
flash("Task deleted successfully!", "success")
return redirect(url_for("index"))


# Create database tables and run the app
if __name__ == "__main__":
with app.app_context():
db.create_all()
app.run(debug=True, port=5001)
10 changes: 10 additions & 0 deletions example/pythonlib/web/2-todo-flask/todo/src/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SelectField, DateField, SubmitField
from wtforms.validators import DataRequired, Length

class TaskForm(FlaskForm):
title = StringField('Title', validators=[DataRequired(), Length(max=100)])
description = TextAreaField('Description')
status = SelectField('Status', choices=[('Pending', 'Pending'), ('Completed', 'Completed')])
deadline = DateField('Deadline', format='%Y-%m-%d', validators=[DataRequired()])
submit = SubmitField('Save')
14 changes: 14 additions & 0 deletions example/pythonlib/web/2-todo-flask/todo/src/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class Task(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text, nullable=True)
status = db.Column(db.String(20), default='Pending') # Options: Pending, Completed
created_at = db.Column(db.DateTime, default=db.func.current_timestamp())
deadline = db.Column(db.Date)

def __repr__(self):
return f'<Task {self.title}>'
21 changes: 21 additions & 0 deletions example/pythonlib/web/2-todo-flask/todo/static/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
body {
background-color: #f8f9fa;
}

.navbar-brand {
font-weight: bold;
}

.table th,
.table td {
text-align: center;
vertical-align: middle;
}

.btn {
margin-right: 5px;
}

.container {
max-width: 900px;
}
31 changes: 31 additions & 0 deletions example/pythonlib/web/2-todo-flask/todo/templates/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flask To-Do App</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">To-Do App Using Mill Build Tool</a>
</div>
</nav>
<div class="container mt-4">
{% with messages = get_flashed_messages(with_categories=True) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
30 changes: 30 additions & 0 deletions example/pythonlib/web/2-todo-flask/todo/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{% extends 'base.html' %}
{% block content %}
<h1 class="mb-4">Task List</h1>
<a href="{{ url_for('add_task') }}" class="btn btn-primary mb-3">Add Task</a>
<table class="table table-bordered">
<thead>
<tr>
<th>#</th>
<th>Title</th>
<th>Status</th>
<th>Deadline</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for task in tasks %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ task.title }}</td>
<td>{{ task.status }}</td>
<td>{{ task.deadline }}</td>
<td>
<a href="{{ url_for('edit_task', task_id=task.id) }}" class="btn btn-sm btn-warning">Edit</a>
<a href="{{ url_for('delete_task', task_id=task.id) }}" class="btn btn-sm btn-danger">Delete</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
24 changes: 24 additions & 0 deletions example/pythonlib/web/2-todo-flask/todo/templates/task.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% block content %}
<h1 class="mb-4">{{ title }}</h1>
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.title.label }}<br>
{{ form.title(class="form-control") }}
</div>
<div class="mb-3">
{{ form.description.label }}<br>
{{ form.description(class="form-control") }}
</div>
<div class="mb-3">
{{ form.status.label }}<br>
{{ form.status(class="form-control") }}
</div>
<div class="mb-3">
{{ form.deadline.label }}<br>
{{ form.deadline(class="form-control") }}
</div>
<button type="submit" class="btn btn-success">{{ form.submit.label }}</button>
</form>
{% endblock %}
1 change: 1 addition & 0 deletions example/pythonlib/web/2-todo-flask/todo/test/src/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Work in Progress...
31 changes: 31 additions & 0 deletions example/pythonlib/web/3-hello-django/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package build
import mill._, pythonlib._

object foo extends PythonModule {

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

def pythonDeps = Seq("django==5.1.4")

}

/** Usage

> ./mill foo.run test main -v 2 # using inbuilt `django test`, `main` is the app name, `-v 2` is verbosity level 2
...
System check identified no issues (0 silenced).
test_index_view (main.tests.TestScript...)
Test that the index view returns a 200 status code ... ok
...
Ran 1 test...
OK
...

> ./mill foo.runBackground runserver

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

> ./mill clean foo.runBackground

*/
Empty file.
16 changes: 16 additions & 0 deletions example/pythonlib/web/3-hello-django/foo/src/app/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
ASGI config for app project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')

application = get_asgi_application()
Loading
Loading