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

Feat(extension): Enable Async Gunicorn workers for Flask and Django e… #747

Merged
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
c0d3102
Feat(extension): Enable Async Gunicorn workers for Flask and Django e…
alithethird Nov 8, 2024
009fe4c
chore(lint): Ran static checkers
alithethird Nov 8, 2024
a6d156f
chore(): Fix flask cli unit test
alithethird Nov 8, 2024
408db1d
chore(docs): Docs lint
alithethird Nov 8, 2024
39671e1
chore(docs): Improved wordlist
alithethird Nov 8, 2024
4220cd7
Merge branch 'main' into flask-django-extention-async-workers
alithethird Nov 15, 2024
8d88e8c
Chore(): Update docs, update cli init test, update `rockcraft.yaml` f…
alithethird Nov 19, 2024
fbd636c
Merge branch 'flask-django-extention-async-workers' of github.com:ali…
alithethird Nov 19, 2024
3280aa6
Chore(): Try to make linter happy
alithethird Nov 19, 2024
c9fab57
Chore(docs): Lint docs
alithethird Nov 19, 2024
a1df1b5
Chore(docs): Update async doc link
alithethird Nov 20, 2024
8446f7e
Merge branch 'main' into flask-django-extention-async-workers
alithethird Nov 20, 2024
0ea1838
Run CI
alithethird Nov 20, 2024
a8e14d2
Merge branch 'flask-django-extention-async-workers' of github.com:ali…
alithethird Nov 20, 2024
99982f8
Chore(Docs): Small doc improvements
alithethird Nov 26, 2024
a645957
Merge branch 'main' into flask-django-extention-async-workers
alithethird Nov 26, 2024
f35db9e
Chore(lint): Doc lint
alithethird Nov 26, 2024
b433599
XMerge branch 'flask-django-extention-async-workers' of github.com:al…
alithethird Nov 26, 2024
56979d4
Chore(docs): Add words to list
alithethird Nov 26, 2024
7c57358
Chore(): Undone import format
alithethird Nov 26, 2024
5b98290
Chore(): Addressed comments.
alithethird Nov 27, 2024
dbd88b3
Chore(): Consistent spacing
alithethird Nov 28, 2024
9338ad9
Chore(Format): Format imports
alithethird Nov 28, 2024
69fc1c1
Merge branch 'main' into flask-django-extention-async-workers
alithethird Nov 28, 2024
7935d3b
Chore(Lint): Add word
alithethird Nov 28, 2024
8837f79
Merge branch 'flask-django-extention-async-workers' of github.com:ali…
alithethird Nov 28, 2024
2665251
Chore(): Update docs, update gunicorn conf for async
alithethird Nov 28, 2024
415807d
Chore(lint): Format
alithethird Nov 28, 2024
f8e9488
Chore(tests): Fix tests
alithethird Nov 28, 2024
c67ea0b
Chore(docs): Fix refs
alithethird Nov 28, 2024
41e4ce9
Chore(docs): Update refs
alithethird Nov 28, 2024
da397aa
Chore(): Updated docs, async conf file
alithethird Nov 29, 2024
071fa2e
Chore(docs): Fix lint
alithethird Nov 29, 2024
b1e4835
Chore(): Add a spread test for async
alithethird Nov 29, 2024
e6b7083
Chore(test): Removed unnecessary fixture
alithethird Nov 29, 2024
7d5b613
Chore(): Format
alithethird Nov 29, 2024
dd95620
Merge main
alithethird Nov 29, 2024
7f35f80
Merge branch 'main' into flask-django-extention-async-workers
alithethird Nov 29, 2024
1c2bf63
Chore(test): Simplify test and fix typo
alithethird Nov 29, 2024
e00dacd
Merge branch 'flask-django-extention-async-workers' of github.com:ali…
alithethird Nov 29, 2024
b2086af
Chore(): Improve code
alithethird Nov 29, 2024
dad6563
Chore(lint): Make linter happy
alithethird Nov 29, 2024
69f740f
Chore(): Make linter happier
alithethird Nov 29, 2024
ab3a635
Merge branch 'main' into flask-django-extention-async-workers
alithethird Nov 29, 2024
b2cd5dc
Chore(test): Fix unit test
alithethird Nov 29, 2024
e82b75e
Merge branch 'flask-django-extention-async-workers' of github.com:ali…
alithethird Nov 29, 2024
aaf21a5
Chore(lint): Make Mypy happy
alithethird Nov 29, 2024
27ce02d
Chore(): Fix shell lint
alithethird Nov 29, 2024
5f58c54
Chore(test): Fix test comment
alithethird Dec 2, 2024
8e9fd49
Chore(test): Add async flask/django tests. Created test dir
alithethird Dec 3, 2024
6eef44a
Chore(lint): Run autoformat
alithethird Dec 3, 2024
0b2be19
Chore(test): Ignore import errors in django data
alithethird Dec 3, 2024
757defc
Chore(test): Fix cli test
alithethird Dec 3, 2024
47b4136
Chore(format): format
alithethird Dec 3, 2024
efb4e23
Chore(): Change async flask implementation
alithethird Dec 4, 2024
16cd05b
Chore(test): Fix spread test
alithethird Dec 4, 2024
c7dbf84
Chore(): Update docs and spread test
alithethird Dec 4, 2024
22d18ea
Chore(test): Removed unnecessary django files
alithethird Dec 4, 2024
a6aaacf
Chore(doc): Lint docs
alithethird Dec 4, 2024
9e0efd1
Update docs/reference/extensions/django-framework.rst
alithethird Dec 5, 2024
fc2caf1
Update docs/reference/extensions/django-framework.rst
alithethird Dec 5, 2024
dbade98
Update docs/reference/extensions/django-framework.rst
alithethird Dec 5, 2024
517532f
Update docs/reference/extensions/django-framework.rst
alithethird Dec 5, 2024
46f82f8
Chore(): Update gevent install check
alithethird Dec 5, 2024
3b4602f
Merge branch 'flask-django-extention-async-workers' of github.com:ali…
alithethird Dec 5, 2024
6fb5bfd
Chore(docs): Style update
alithethird Dec 5, 2024
9a25b90
Merge branch 'main' into flask-django-extention-async-workers
alithethird Dec 5, 2024
e6e6d8d
Chore(): Updated pip package check
alithethird Dec 5, 2024
722e5ff
Merge branch 'flask-django-extention-async-workers' of github.com:ali…
alithethird Dec 5, 2024
eac7d06
Chore(Deps): Add `packaging` dependency
alithethird Dec 5, 2024
18b16fc
Chore(lint): Format code
alithethird Dec 5, 2024
6bc31bb
Chore(format): Format
alithethird Dec 5, 2024
9a6dc45
Chore(docs): Format doc
alithethird Dec 5, 2024
6956da0
Chore(): Fix spread test, change async check
alithethird Dec 5, 2024
537a19d
Chore(): Except specific exception
alithethird Dec 5, 2024
b4ae759
Chore(): Uncomment restore
alithethird Dec 5, 2024
6b6f00c
Update docs/reference/extensions/django-framework.rst
alithethird Dec 9, 2024
4dc5791
Update docs/reference/extensions/flask-framework.rst
alithethird Dec 9, 2024
7612690
Chore(): Addressed comments. Add comment to test_cli.py. fix doc issu…
alithethird Dec 9, 2024
b993abb
Chore(test): Add unit async gunicorn tests
alithethird Dec 9, 2024
508afc0
Merge branch 'main' into flask-django-extention-async-workers
alithethird Dec 9, 2024
aeee726
Chore(lint): Format code
alithethird Dec 9, 2024
e6a8d44
Chore(): Changed copy_tree() to shutil.copytree()
alithethird Dec 10, 2024
ae05f6d
chore(doc): Fix doc format
alithethird Dec 10, 2024
cdd9134
chore(doc): Format
alithethird Dec 10, 2024
6aba111
chore(doc): Format
alithethird Dec 10, 2024
9846735
chore(doc): Format
alithethird Dec 10, 2024
5617d29
chore(doc): Format
alithethird Dec 10, 2024
b6550ef
chore(test): Update tests
alithethird Dec 10, 2024
3b214dc
chore(doc): Change testing port in doc
alithethird Dec 11, 2024
ead9f9c
chore(update): Update statsd-exporter version in flask/django plugin
alithethird Dec 13, 2024
8454a69
Merge branch 'flask-django-extention-async-workers' of github.com:ali…
alithethird Dec 13, 2024
97e8bcb
chore(): Revert version change in statsd
alithethird Dec 13, 2024
ec5db1e
Merge branch 'main' into flask-django-extention-async-workers
alithethird Dec 16, 2024
d517d98
chore(doc): Update async docs
alithethird Dec 17, 2024
75f95c1
Merge branch 'main' into flask-django-extention-async-workers
alithethird Dec 17, 2024
2b4a3bb
Update docs/reference/extensions/django-framework.rst
alithethird Dec 18, 2024
1321bd2
Update docs/reference/extensions/flask-framework.rst
alithethird Dec 18, 2024
cae3f7d
Merge branch 'main' into flask-django-extention-async-workers
alithethird Dec 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/.wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ AMD
amd
ARGS
ASGI
async
Autotools
autotools
boolean
Expand Down Expand Up @@ -43,10 +44,12 @@ filesystem
filesystems
fs
gc
gevent
GiB
GID
github
GPG
gunicorn
Gunicorn
gzipped
hardcoded
Expand Down Expand Up @@ -143,6 +146,7 @@ triaged
ubuntu
unbuilt
UID
uncomment
usrmerge
Uvicorn
VENV
Expand Down
27 changes: 27 additions & 0 deletions docs/reference/extensions/django-framework.rst
alithethird marked this conversation as resolved.
Show resolved Hide resolved
alithethird marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,33 @@ application. In the following example we use it to specify ``libpq-dev``:
# list required packages or slices for your Django application below.
- libpq-dev

.. warning::
You can only use 1 of the dependencies parts at a time.
alithethird marked this conversation as resolved.
Show resolved Hide resolved
(eg. `parts-django-framework-async-dependencies`_ or
`parts-django-framework-dependencies-stage-packages`_)
alithethird marked this conversation as resolved.
Show resolved Hide resolved

``parts`` > ``django-framework/async-dependencies``
=================================================================

You can use this key to specify that you want to use async gunicorn workers in
your Django application.

Just uncomment the following lines:
.. code-block:: yaml

parts:
django-framework/async-dependencies:
python-packages:
- gunicorn[gevent]

If your project needs additional debs to run, you can add them to
``stage-packages`` just like it is done in `django-framework/dependencies`_.

.. warning::
You can only use 1 of the dependencies parts at a time.
(eg. `parts-django-framework-async-dependencies`_ or
`parts-django-framework-dependencies-stage-packages`_)

Useful links
============

Expand Down
27 changes: 27 additions & 0 deletions docs/reference/extensions/flask-framework.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,33 @@ application. In the following example we use it to specify ``libpq-dev``:
# list required packages or slices for your flask app below.
- libpq-dev

.. warning::
You can only use 1 of the dependencies parts at a time.
(eg. `parts-flask-framework-async-dependencies`_ or
`parts-flask-framework-dependencies-stage-packages`_)

``parts`` > ``flask-framework/async-dependencies``
=================================================================

You can use this key to specify that you want to use async gunicorn workers in
your Flask application.

Just uncomment the following lines:
.. code-block:: yaml

parts:
flask-framework/async-dependencies:
python-packages:
- gunicorn[gevent]

If your project needs additional debs to run, you can add them to
``stage-packages`` just like it is done in `flask-framework/dependencies`_.

.. warning::
You can only use 1 of the dependencies parts at a time.
(eg. `parts-flask-framework-async-dependencies`_ or
`parts-flask-framework-dependencies-stage-packages`_)

``parts`` > ``flask-framework/install-app`` > ``prime``
=======================================================

Expand Down
10 changes: 10 additions & 0 deletions rockcraft/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ class InitCommand(AppCommand):
# - flask/app/templates
# - flask/app/static

# uncomment this section to enable the async workers for Gunicorn.
# flask-framework/async-dependencies:
# python-packages:
# - gunicorn[gevent]

# you may need Ubuntu packages to build a python dependency. Add them here if necessary.
# flask-framework/dependencies:
# build-packages:
Expand Down Expand Up @@ -208,6 +213,11 @@ class InitCommand(AppCommand):
# stage-packages:
# # list required packages or slices for your Django application below.
# - libpq-dev
# uncomment this section to enable the async workers for Gunicorn.
# django-framework/async-dependencies:
# python-packages:
# - gunicorn[gevent]

"""
)
),
Expand Down
37 changes: 35 additions & 2 deletions rockcraft/extensions/gunicorn.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,28 @@ def _gen_parts(self) -> dict:
stage_packages = ["python3.10-venv_ensurepip"]
build_environment = [{"PARTS_PYTHON_INTERPRETER": "python3.10"}]

parts: dict[str, Any] = {
sync_dependencies: dict[str, Any] = {
f"{self.framework}-framework/dependencies": {
"plugin": "python",
"stage-packages": stage_packages,
"source": ".",
"python-packages": ["gunicorn"],
"python-requirements": ["requirements.txt"],
"build-environment": build_environment,
},
}
}
async_dependencies: dict[str, Any] = {
f"{self.framework}-framework/async-dependencies": {
"plugin": "python",
"stage-packages": stage_packages,
"source": ".",
"python-packages": ["gunicorn[gevent]"],
"python-requirements": ["requirements.txt"],
"build-environment": build_environment,
}
}

parts: dict[str, Any] = {
f"{self.framework}-framework/install-app": self.gen_install_app_part(),
f"{self.framework}-framework/config-files": {
"plugin": "dump",
Expand All @@ -95,6 +108,13 @@ def _gen_parts(self) -> dict:
"source": "https://github.com/prometheus/statsd_exporter.git",
},
}
if f"{self.framework}-framework/async-dependencies" in self.yaml_data.get(
"parts", {}
):
parts = dict(async_dependencies, **parts)
else:
parts = dict(sync_dependencies, **parts)

if self.yaml_data["base"] == "bare":
parts[f"{self.framework}-framework/runtime"] = {
"plugin": "nil",
Expand Down Expand Up @@ -273,12 +293,25 @@ def _requirements_txt_error_messages(self) -> list[str]:

return []

def _dependencies_error_messages(self) -> list[str]:
"""Ensure only 1 of the dependencies parts is defined."""
yaml_parts = self.yaml_data.get("parts", {})

if yaml_parts.get(
alithethird marked this conversation as resolved.
Show resolved Hide resolved
f"{self.framework}-framework/async-dependencies", None
) and yaml_parts.get(f"{self.framework}-framework/dependencies", None):
return [
f"Cannot have both sync and async dependencies. https://bit.ly/{self.framework}-async-doc"
alithethird marked this conversation as resolved.
Show resolved Hide resolved
]
return []

@override
def check_project(self) -> None:
"""Ensure this extension can apply to the current rockcraft project."""
error_messages = self._requirements_txt_error_messages()
if not self.yaml_data.get("services", {}).get("flask", {}).get("command"):
error_messages += self._wsgi_path_error_messages()
error_messages += self._dependencies_error_messages()
if error_messages:
raise ExtensionError(
"\n".join("- " + message for message in error_messages),
Expand Down
2 changes: 1 addition & 1 deletion rockcraft/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ def load_project(filename: Path) -> dict[str, Any]:
msg = err.strerror or "unknown"
if err.filename:
msg = f"{msg}: {err.filename!r}."
raise ProjectLoadError(msg) from err
raise ProjectLoadError(str(msg)) from err

return transform_yaml(filename.parent, yaml_data)

Expand Down
182 changes: 182 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,188 @@ def new_dir(tmpdir):
os.chdir(cwd)


@pytest.fixture()
alithethird marked this conversation as resolved.
Show resolved Hide resolved
def django_dir(new_dir):
"""Change to a new temporary directory."""
(new_dir / "requirements.txt").write_text("Django", encoding="utf-8")
new_dir.mkdir("test_name")
new_dir.mkdir("test_name/test_name")
(new_dir / "test_name/manage.py").write_text(
"""\
#!/usr/bin/env python
\"\"\"Django's command-line utility for administrative tasks.\"\"\"
import os
import sys


def main():
\"\"\"Run administrative tasks.\"\"\"
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_name.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
\"Couldn't import Django. Are you sure it's installed and \"
\"available on your PYTHONPATH environment variable? Did you \"
\"forget to activate a virtual environment?\"
) from exc
execute_from_command_line(sys.argv)


if __name__ == '__main__':
main()

""",
encoding="utf-8",
)
(new_dir / "test_name/test_name/urls.py").write_text(
"""\
from django.contrib import admin
from django.urls import path

urlpatterns = [
path('admin/', admin.site.urls),
]
""",
encoding="utf-8",
)
(new_dir / "test_name/test_name/__init__.py").write_text("", encoding="utf-8")
(new_dir / "test_name/test_name/settings.py").write_text(
"""\
\"\"\"
Django settings for test_name project.

Generated by 'django-admin startproject' using Django 5.1.3.

For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
\"\"\"

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-s!661fk_mhkv!thlq1j&o+%7&%(djz+ir=6^+o$jtgbf(_2t_s'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'test_name.urls'

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]

WSGI_APPLICATION = 'test_name.wsgi.application'


# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}


# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]


# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/

STATIC_URL = 'static/'

# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
""",
encoding="utf-8",
)

(new_dir / "test_name/test_name/wsgi.py").write_text(
"import os\nfrom django.core.wsgi import get_wsgi_application\nos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_app.settings')\napplication = get_wsgi_application()\n",
encoding="utf-8",
)
return new_dir


@pytest.fixture(autouse=True)
def temp_xdg(tmpdir, mocker):
"""Use a temporary location for XDG directories."""
Expand Down
Loading
Loading