Skip to content

Commit 32b0cd3

Browse files
authored
Merge pull request #6 from Never-Over/celery-django-settings
Refactor Django Celery configuration
2 parents d8a3d6b + 716cfdf commit 32b0cd3

File tree

11 files changed

+127
-61
lines changed

11 files changed

+127
-61
lines changed

bridge/cli/init/render.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
from pathlib import Path
3+
from typing import Optional
34

45
from pydantic import BaseModel
56

@@ -10,13 +11,31 @@
1011
start_worker_sh_template,
1112
)
1213
from bridge.console import console
14+
from bridge.framework.base import Framework
1315
from bridge.utils.filesystem import (
1416
resolve_dot_bridge,
1517
resolve_project_dir,
1618
set_executable,
1719
)
1820

1921

22+
def detect_framework() -> Framework:
23+
# TODO: auto-detect framework (assuming Django)
24+
return Framework.DJANGO
25+
26+
27+
def detect_django_settings_module(project_name: str = "") -> str:
28+
settings_path = f"{project_name}/settings.py"
29+
if os.path.exists(settings_path):
30+
return f"{project_name}.settings"
31+
else:
32+
# TODO: validate input
33+
return console.input(
34+
"Please provide the path to your"
35+
" Django settings module (ex: myapp.settings):\n> "
36+
)
37+
38+
2039
def detect_application_callable(project_name: str = "") -> str:
2140
wsgi_path = f"{project_name}/wsgi.py"
2241
asgi_path = f"{project_name}/asgi.py"
@@ -25,34 +44,48 @@ def detect_application_callable(project_name: str = "") -> str:
2544
elif os.path.exists(asgi_path):
2645
return f"{project_name}.asgi:application"
2746
else:
47+
# TODO: validate input
2848
return console.input(
2949
"Please provide the path to your WSGI or ASGI application callable "
3050
"(ex: myapp.wsgi:application):\n> "
3151
)
3252

3353

54+
class DjangoConfig(BaseModel):
55+
settings_module: str
56+
57+
3458
class RenderPlatformInitConfig(BaseModel):
59+
framework: Framework = Framework.DJANGO
3560
project_name: str
3661
app_path: str
3762
bridge_path: str
63+
django_config: Optional[DjangoConfig] = None
3864

3965

4066
def build_render_init_config() -> RenderPlatformInitConfig:
4167
# NOTE: this method may request user input directly on the CLI
4268
# to determine configuration when it cannot be auto-detected
69+
framework = detect_framework()
4370
project_name = resolve_project_dir().name
4471
app_path = detect_application_callable(project_name=project_name)
45-
# May be able to remove this call since all scripts, YAML have moved to root
4672
bridge_path = resolve_dot_bridge()
47-
return RenderPlatformInitConfig(
73+
init_config = RenderPlatformInitConfig(
4874
project_name=project_name, app_path=app_path, bridge_path=str(bridge_path)
4975
)
5076

77+
# Provide framework-specific configuration
78+
if framework == Framework.DJANGO:
79+
settings_module = detect_django_settings_module(project_name=project_name)
80+
init_config.django_config = DjangoConfig(settings_module=settings_module)
81+
82+
return init_config
83+
5184

5285
def initialize_render_platform(config: RenderPlatformInitConfig):
5386
build_sh_path = Path("./render-build.sh")
5487
with build_sh_path.open(mode="w") as f:
55-
f.write(build_sh_template())
88+
f.write(build_sh_template(framework=config.framework))
5689
set_executable(build_sh_path)
5790

5891
start_sh_path = Path("./render-start.sh")
@@ -62,13 +95,17 @@ def initialize_render_platform(config: RenderPlatformInitConfig):
6295

6396
start_worker_sh_path = Path("./render-start-worker.sh")
6497
with start_worker_sh_path.open(mode="w") as f:
65-
f.write(start_worker_sh_template())
98+
f.write(start_worker_sh_template(framework=config.framework))
6699
set_executable(start_worker_sh_path)
67100

68101
with open("render.yaml", "w") as f:
69102
f.write(
70103
render_yaml_template(
104+
framework=config.framework,
71105
service_name=config.project_name,
72106
database_name=f"{config.project_name}_db",
107+
django_settings_module=config.django_config.settings_module
108+
if config.django_config
109+
else "",
73110
)
74111
)

bridge/cli/init/templates/render__yaml.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from bridge.framework.base import Framework
12
from bridge.utils.sanitize import sanitize_postgresql_identifier
23

34
template = """services:
@@ -10,8 +11,6 @@
1011
envVars:
1112
- key: BRIDGE_PLATFORM
1213
value: render
13-
- key: BRIDGE_PROJECT_NAME
14-
value: {service_name}
1514
- key: SECRET_KEY
1615
generateValue: true
1716
- key: WEB_CONCURRENCY
@@ -39,8 +38,8 @@
3938
envVars:
4039
- key: BRIDGE_PLATFORM
4140
value: render
42-
- key: BRIDGE_PROJECT_NAME
43-
value: {service_name}
41+
- key: DJANGO_SETTINGS_MODULE
42+
value: {django_settings_module}
4443
- key: SECRET_KEY
4544
generateValue: true
4645
- key: TASK_CONCURRENCY
@@ -66,12 +65,29 @@
6665

6766

6867
def render_yaml_template(
69-
service_name: str, database_name: str = "", database_user: str = ""
68+
framework: Framework,
69+
service_name: str,
70+
database_name: str = "",
71+
database_user: str = "",
72+
django_settings_module: str = "",
7073
) -> str:
74+
if framework != Framework.DJANGO:
75+
# TODO: use a real templating engine to allow flexibility across frameworks
76+
raise NotImplementedError(
77+
f"Unsupported framework for Render platform: {framework}"
78+
)
79+
80+
if not django_settings_module:
81+
raise ValueError(
82+
"Failed to template render.yaml:"
83+
" DJANGO_SETTINGS_MODULE is required for Django projects"
84+
)
85+
7186
database_name = database_name or service_name
7287
database_user = database_user or service_name
7388
return template.format(
7489
service_name=service_name,
7590
database_name=sanitize_postgresql_identifier(database_name),
7691
database_user=sanitize_postgresql_identifier(database_user),
92+
django_settings_module=django_settings_module,
7793
)

bridge/cli/init/templates/render_build__sh.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from bridge.framework.base import Framework
2+
13
template = """#!/usr/bin/env bash
24
# Exit on error
35
set -o errexit
@@ -16,6 +18,9 @@
1618
"""
1719

1820

19-
def build_sh_template() -> str:
20-
# TODO: support other package managers, frameworks, etc.
21+
def build_sh_template(framework: Framework) -> str:
22+
if framework != Framework.DJANGO:
23+
raise NotImplementedError(
24+
f"Unsupported framework for Render platform: {framework}"
25+
)
2126
return template.format()
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
from bridge.framework.base import Framework
2+
13
template = """#!/usr/bin/env bash
2-
celery -A bridge.service.celery worker -l INFO --concurrency="${{TASK_CONCURRENCY:-4}}"
4+
celery -A bridge.service.django_celery worker -l INFO --concurrency="${{TASK_CONCURRENCY:-4}}"
35
"""
46

57

6-
def start_worker_sh_template() -> str:
8+
def start_worker_sh_template(framework: Framework) -> str:
9+
if framework != Framework.DJANGO:
10+
raise NotImplementedError(
11+
f"Unsupported framework for Render platform: {framework}"
12+
)
713
return template.format()

bridge/framework/base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
from abc import ABC, abstractmethod
3+
from enum import Enum
34
from typing import Any
45

56
import docker
@@ -9,7 +10,15 @@
910
from bridge.service.redis import RedisConfig, RedisService
1011

1112

13+
class Framework(Enum):
14+
DJANGO = "django"
15+
FLASK = "flask"
16+
FASTAPI = "fastapi"
17+
18+
1219
class FrameWorkHandler(ABC):
20+
FRAMEWORK: Framework = NotImplemented
21+
1322
def __init__(
1423
self,
1524
project_name: str,

bridge/framework/django.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
from typing import Any
55

66
from bridge.console import log_task, log_warning
7-
from bridge.framework.base import FrameWorkHandler
7+
from bridge.framework.base import Framework, FrameWorkHandler
88
from bridge.platform import Platform
99
from bridge.platform.postgres import build_postgres_environment
1010
from bridge.platform.redis import build_redis_environment
1111

1212

1313
class DjangoHandler(FrameWorkHandler):
14+
FRAMEWORK = Framework.DJANGO
15+
1416
def is_remote(self) -> bool:
1517
# Django's DEBUG mode should be disabled in production,
1618
# so we use it to differentiate between running locally
@@ -112,21 +114,22 @@ def configure_staticfiles(self, platform: Platform):
112114
middleware.insert(0, "whitenoise.middleware.WhiteNoiseMiddleware")
113115

114116
def configure_worker(self, platform: Platform) -> None:
117+
# This will make sure the app is always imported when
118+
# Django starts so that shared_task will use this app.
119+
from bridge.service.django_celery import app # noqa: F401 type: ignore
120+
115121
environment = build_redis_environment(platform)
116122
self.framework_locals["CELERY_BROKER_URL"] = environment.url
117123
self.framework_locals["CELERY_RESULT_BACKEND"] = environment.url
118124

119125
def start_local_worker(self) -> None:
120-
# Confirm we are in a `runserver` command
121-
if "runserver" in sys.argv or "runserver_plus" in sys.argv:
122-
# This will make sure the app is always imported when
123-
# Django starts so that shared_task will use this app.
124-
from bridge.service.celery import app # noqa: F401 type: ignore
125-
126+
# Confirm we are in a command which expects Celery to be available
127+
expected_command_args = {"runserver", "runserver_plus", "shell", "shell_plus"}
128+
if set(sys.argv) & expected_command_args:
126129
with log_task("Starting local worker", "Local worker started"):
127130
try:
128131
subprocess.run(
129-
["celery", "-A", "bridge.service.celery", "status"],
132+
["celery", "-A", "bridge.service.django_celery", "status"],
130133
check=True,
131134
stdout=subprocess.DEVNULL,
132135
stderr=subprocess.STDOUT,
@@ -137,7 +140,7 @@ def start_local_worker(self) -> None:
137140
[
138141
"celery",
139142
"-A",
140-
"bridge.service.celery",
143+
"bridge.service.django_celery",
141144
"worker",
142145
"-l",
143146
"INFO",

bridge/service/celery.py

Lines changed: 0 additions & 37 deletions
This file was deleted.

bridge/service/django_celery.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import os
2+
3+
from celery import Celery
4+
5+
if "DJANGO_SETTINGS_MODULE" not in os.environ:
6+
raise ValueError(
7+
"DJANGO_SETTINGS_MODULE must be set in the environment to run Celery"
8+
)
9+
10+
project_name = os.environ["DJANGO_SETTINGS_MODULE"].split(".")[0]
11+
app = Celery(project_name)
12+
13+
# Using a string here means the worker doesn't have to serialize
14+
# the configuration object to child processes.
15+
# - namespace='CELERY' means all celery-related configuration keys
16+
# should have a `CELERY_` prefix.
17+
app.config_from_object("django.conf:settings", namespace="CELERY")
18+
19+
# Load task modules from all registered Django apps.
20+
app.autodiscover_tasks()
21+
22+
23+
@app.task(bind=True, ignore_result=True)
24+
def debug_task(self):
25+
print(f"Request: {self.request!r}")

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "python-bridge"
3-
version = "0.0.20"
3+
version = "0.0.21"
44
authors = [
55
{ name="Caelean Barnes", email="[email protected]" },
66
{ name="Evan Doyle", email="[email protected]" },
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
#!/usr/bin/env bash
2-
celery -A bridge.service.celery worker -l INFO --concurrency="${TASK_CONCURRENCY:-4}"
2+
celery -A bridge.service.django_celery worker -l INFO --concurrency="${TASK_CONCURRENCY:-4}"

0 commit comments

Comments
 (0)