bluefox-cli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ name: Publish to PyPI
2
+ on:
3
+ push:
4
+ tags: ["v*"]
5
+
6
+ jobs:
7
+ publish:
8
+ runs-on: ubuntu-latest
9
+ environment: pypi
10
+ permissions:
11
+ contents: read
12
+ id-token: write
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+ - uses: astral-sh/setup-uv@v5
19
+ - run: uv build
20
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,207 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: bluefox-cli
3
+ Version: 0.1.0
4
+ Summary: Scaffolding tool for the Bluefox Stack — one command to generate a complete FastAPI project.
5
+ Project-URL: Homepage, https://github.com/blue-fox-software/bluefox-cli
6
+ Project-URL: Repository, https://github.com/blue-fox-software/bluefox-cli
7
+ License-Expression: MIT
8
+ Requires-Python: >=3.12
9
+ Requires-Dist: click>=8.0
File without changes
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "bluefox-cli"
7
+ version = "0.1.0"
8
+ description = "Scaffolding tool for the Bluefox Stack — one command to generate a complete FastAPI project."
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "MIT"
12
+ dependencies = [
13
+ "click>=8.0",
14
+ ]
15
+
16
+ [project.scripts]
17
+ bluefox = "bluefox_cli.main:cli"
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/blue-fox-software/bluefox-cli"
21
+ Repository = "https://github.com/blue-fox-software/bluefox-cli"
22
+
23
+ [dependency-groups]
24
+ dev = [
25
+ "pytest>=8.0",
26
+ ]
27
+
28
+ [tool.pytest.ini_options]
29
+ testpaths = ["tests"]
@@ -0,0 +1,3 @@
1
+ """Bluefox CLI — scaffolding tool for the Bluefox Stack."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,57 @@
1
+ import os
2
+ import subprocess
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import click
7
+
8
+ from bluefox_cli.templates import TEMPLATES
9
+
10
+
11
+ @click.group()
12
+ def cli():
13
+ """Bluefox Stack CLI."""
14
+
15
+
16
+ @cli.command()
17
+ @click.argument("project_name")
18
+ def init(project_name: str):
19
+ """Create a new Bluefox project."""
20
+ project_dir = Path.cwd() / project_name
21
+
22
+ if project_dir.exists():
23
+ click.echo(f"Error: directory '{project_name}' already exists.", err=True)
24
+ sys.exit(1)
25
+
26
+ click.echo(f"Creating project '{project_name}'...")
27
+
28
+ # Create directories
29
+ project_dir.mkdir()
30
+ (project_dir / "migrations" / "versions").mkdir(parents=True)
31
+ (project_dir / "tests").mkdir()
32
+
33
+ # Write all template files
34
+ for rel_path, content_fn in TEMPLATES.items():
35
+ file_path = project_dir / rel_path
36
+ file_path.parent.mkdir(parents=True, exist_ok=True)
37
+ file_path.write_text(content_fn(project_name))
38
+
39
+ # Initialize uv project and add dependencies
40
+ _run(["uv", "init", "--no-readme"], cwd=project_dir)
41
+ _run(["uv", "add", "bluefox-core", "alembic", "asyncpg"], cwd=project_dir)
42
+ _run(["uv", "add", "--dev", "bluefox-test", "pytest"], cwd=project_dir)
43
+
44
+ click.echo("")
45
+ click.echo(f"Project '{project_name}' created successfully!")
46
+ click.echo("")
47
+ click.echo("Next steps:")
48
+ click.echo(f" cd {project_name}")
49
+ click.echo(" make dev")
50
+
51
+
52
+ def _run(cmd: list[str], cwd: Path) -> None:
53
+ result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
54
+ if result.returncode != 0:
55
+ click.echo(f"Error running {' '.join(cmd)}:", err=True)
56
+ click.echo(result.stderr, err=True)
57
+ sys.exit(1)
File without changes
@@ -0,0 +1,294 @@
1
+ """Templates for generated project files.
2
+
3
+ Each function takes project_name and returns the file content as a string.
4
+ """
5
+
6
+
7
+ def _main_py(project_name: str) -> str:
8
+ return """from bluefox_core import BluefoxSettings, create_bluefox_app
9
+
10
+
11
+ def create_app() -> object:
12
+ settings = BluefoxSettings(MODELS_MODULE="models")
13
+ return create_bluefox_app(settings)
14
+
15
+
16
+ app = create_app()
17
+ """
18
+
19
+
20
+ def _models_py(project_name: str) -> str:
21
+ return """from sqlalchemy import Column, Integer, String
22
+ from bluefox_core import BluefoxBase
23
+
24
+
25
+ class Example(BluefoxBase):
26
+ __tablename__ = "examples"
27
+
28
+ id = Column(Integer, primary_key=True)
29
+ name = Column(String, nullable=False)
30
+ """
31
+
32
+
33
+ def _env_example(project_name: str) -> str:
34
+ app_name = project_name.replace("_", "-")
35
+ return f"""# Application
36
+ APP_NAME={app_name}
37
+ ENVIRONMENT=development
38
+ DEBUG=false
39
+ LOG_LEVEL=INFO
40
+ SECRET_KEY=change-me-in-production
41
+
42
+ # Database (PostgreSQL)
43
+ DATABASE_URL=postgresql://myuser:mypass@localhost:5432/{project_name}
44
+
45
+ # Redis (optional)
46
+ REDIS_URL=
47
+ """
48
+
49
+
50
+ def _env(project_name: str) -> str:
51
+ app_name = project_name.replace("_", "-")
52
+ return f"""# Application
53
+ APP_NAME={app_name}
54
+ ENVIRONMENT=development
55
+ DEBUG=true
56
+ LOG_LEVEL=DEBUG
57
+ SECRET_KEY=dev-secret-key
58
+
59
+ # Database (PostgreSQL)
60
+ DATABASE_URL=postgresql://myuser:mypass@localhost:5432/{project_name}
61
+
62
+ # Redis (optional)
63
+ REDIS_URL=
64
+ """
65
+
66
+
67
+ def _makefile(project_name: str) -> str:
68
+ db_url = f"postgresql+asyncpg://myuser:mypass@localhost:5432/{project_name}"
69
+ return f""".PHONY: dev dev-down run migrate migrate-make test
70
+
71
+ DB_URL = {db_url}
72
+
73
+ dev:
74
+ docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
75
+
76
+ dev-down:
77
+ docker compose -f docker-compose.yml -f docker-compose.dev.yml down
78
+
79
+ run:
80
+ DATABASE_URL=$(DB_URL) uv run uvicorn main:app --reload --port 8000
81
+
82
+ migrate:
83
+ DATABASE_URL=$(DB_URL) uv run python -m alembic upgrade head
84
+
85
+ migrate-make:
86
+ DATABASE_URL=$(DB_URL) uv run python -m alembic revision --autogenerate -m "$(name)"
87
+
88
+ test:
89
+ uv run pytest
90
+ """
91
+
92
+
93
+ def _dockerfile(project_name: str) -> str:
94
+ return """# --- Build stage ---
95
+ FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder
96
+ WORKDIR /app
97
+ COPY pyproject.toml uv.lock ./
98
+ RUN uv sync --frozen --no-dev --no-install-project
99
+ COPY . .
100
+ RUN uv sync --frozen --no-dev
101
+
102
+ # --- Runtime stage ---
103
+ FROM python:3.12-slim-bookworm
104
+ WORKDIR /app
105
+ RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* \\
106
+ && groupadd --system app && useradd --system --gid app app
107
+ COPY --from=builder /app/.venv /app/.venv
108
+ COPY . .
109
+ ENV PATH="/app/.venv/bin:$PATH"
110
+ USER app
111
+ EXPOSE 8000
112
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
113
+ """
114
+
115
+
116
+ def _docker_compose(project_name: str) -> str:
117
+ return """services:
118
+ migrate:
119
+ build: .
120
+ environment:
121
+ - DATABASE_URL=${DATABASE_URL:-}
122
+ command: ["python", "-m", "alembic", "upgrade", "head"]
123
+ restart: "no"
124
+ networks:
125
+ - dokploy-network
126
+
127
+ app:
128
+ build: .
129
+ restart: unless-stopped
130
+ ports:
131
+ - "${APP_PORT:-8000}:8000"
132
+ environment:
133
+ - DATABASE_URL=${DATABASE_URL:-}
134
+ - SECRET_KEY=${SECRET_KEY:-change-me-in-production}
135
+ depends_on:
136
+ migrate:
137
+ condition: service_completed_successfully
138
+ healthcheck:
139
+ test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
140
+ interval: 30s
141
+ timeout: 5s
142
+ start_period: 10s
143
+ retries: 3
144
+ networks:
145
+ - dokploy-network
146
+
147
+ networks:
148
+ dokploy-network:
149
+ external: true
150
+ """
151
+
152
+
153
+ def _docker_compose_dev(project_name: str) -> str:
154
+ return f"""services:
155
+ db:
156
+ image: postgres:17-alpine
157
+ restart: unless-stopped
158
+ environment:
159
+ POSTGRES_USER: myuser
160
+ POSTGRES_PASSWORD: mypass
161
+ POSTGRES_DB: {project_name}
162
+ ports:
163
+ - "5432:5432"
164
+ volumes:
165
+ - pgdata:/var/lib/postgresql/data
166
+ healthcheck:
167
+ test: ["CMD-SHELL", "pg_isready -U myuser -d {project_name}"]
168
+ interval: 2s
169
+ timeout: 3s
170
+ retries: 10
171
+
172
+ migrate:
173
+ depends_on:
174
+ db:
175
+ condition: service_healthy
176
+ environment:
177
+ - DATABASE_URL=postgresql+asyncpg://myuser:mypass@db:5432/{project_name}
178
+ networks:
179
+ - default
180
+
181
+ app:
182
+ environment:
183
+ - DATABASE_URL=postgresql+asyncpg://myuser:mypass@db:5432/{project_name}
184
+ - ENVIRONMENT=development
185
+ - DEBUG=true
186
+ volumes:
187
+ - .:/app
188
+ command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
189
+ networks:
190
+ - default
191
+
192
+ networks:
193
+ dokploy-network:
194
+ driver: bridge
195
+
196
+ volumes:
197
+ pgdata:
198
+ """
199
+
200
+
201
+ def _alembic_ini(project_name: str) -> str:
202
+ return """[alembic]
203
+ script_location = migrations
204
+ prepend_sys_path = .
205
+ # sqlalchemy.url is set via DATABASE_URL env var in migrations/env.py
206
+
207
+ [loggers]
208
+ keys = root,sqlalchemy,alembic
209
+
210
+ [handlers]
211
+ keys = console
212
+
213
+ [formatters]
214
+ keys = generic
215
+
216
+ [logger_root]
217
+ level = WARN
218
+ handlers = console
219
+ qualname =
220
+
221
+ [logger_sqlalchemy]
222
+ level = WARN
223
+ handlers =
224
+ qualname = sqlalchemy.engine
225
+
226
+ [logger_alembic]
227
+ level = INFO
228
+ handlers =
229
+ qualname = alembic
230
+
231
+ [handler_console]
232
+ class = StreamHandler
233
+ args = (sys.stderr,)
234
+ level = NOTSET
235
+ formatter = generic
236
+
237
+ [formatter_generic]
238
+ format = %(levelname)-5.5s [%(name)s] %(message)s
239
+ datefmt = %H:%M:%S
240
+ """
241
+
242
+
243
+ def _migrations_env_py(project_name: str) -> str:
244
+ return """from alembic import context
245
+ from bluefox_core.migrations import configure_alembic
246
+
247
+ configure_alembic(context)
248
+ """
249
+
250
+
251
+ def _conftest_py(project_name: str) -> str:
252
+ return """from bluefox_test import bluefox_test_setup
253
+ from models import Example # noqa: F401 — register models
254
+ from bluefox_core import BluefoxBase
255
+
256
+ globals().update(bluefox_test_setup(base=BluefoxBase, app_factory="main:create_app"))
257
+ """
258
+
259
+
260
+ def _test_health_py(project_name: str) -> str:
261
+ return """import pytest
262
+
263
+
264
+ @pytest.mark.asyncio
265
+ async def test_health(client):
266
+ response = await client.get("/health")
267
+ assert response.status_code == 200
268
+ data = response.json()
269
+ assert data["status"] == "ok"
270
+ """
271
+
272
+
273
+ def _pyproject_toml(project_name: str) -> str:
274
+ # This is intentionally empty — uv init will create it,
275
+ # then uv add will populate dependencies.
276
+ # We don't write this file.
277
+ return ""
278
+
279
+
280
+ # Map of relative paths to template functions
281
+ TEMPLATES: dict[str, callable] = {
282
+ "main.py": _main_py,
283
+ "models.py": _models_py,
284
+ ".env.example": _env_example,
285
+ ".env": _env,
286
+ "Makefile": _makefile,
287
+ "Dockerfile": _dockerfile,
288
+ "docker-compose.yml": _docker_compose,
289
+ "docker-compose.dev.yml": _docker_compose_dev,
290
+ "alembic.ini": _alembic_ini,
291
+ "migrations/env.py": _migrations_env_py,
292
+ "tests/conftest.py": _conftest_py,
293
+ "tests/test_health.py": _test_health_py,
294
+ }
File without changes
@@ -0,0 +1,159 @@
1
+ """Tests for the bluefox init command."""
2
+
3
+ import os
4
+
5
+ from click.testing import CliRunner
6
+
7
+ from bluefox_cli.main import cli
8
+
9
+
10
+ def test_init_creates_project(tmp_path):
11
+ runner = CliRunner()
12
+ os.chdir(tmp_path)
13
+ result = runner.invoke(cli, ["init", "myapp"], catch_exceptions=False)
14
+
15
+ # Command should fail because uv add can't resolve bluefox-core in test env,
16
+ # but the directory and files should still be created before that point.
17
+ # So we test file generation separately.
18
+ project_dir = tmp_path / "myapp"
19
+ assert project_dir.exists()
20
+
21
+
22
+ def test_init_creates_all_expected_files(tmp_path):
23
+ """Verify all expected files are written (before uv commands run)."""
24
+ from bluefox_cli.templates import TEMPLATES
25
+
26
+ runner = CliRunner()
27
+ os.chdir(tmp_path)
28
+ # This will fail at the uv step, but files should exist
29
+ runner.invoke(cli, ["init", "myapp"])
30
+
31
+ project_dir = tmp_path / "myapp"
32
+ for rel_path in TEMPLATES:
33
+ assert (project_dir / rel_path).exists(), f"Missing: {rel_path}"
34
+
35
+
36
+ def test_init_creates_migrations_versions_dir(tmp_path):
37
+ runner = CliRunner()
38
+ os.chdir(tmp_path)
39
+ runner.invoke(cli, ["init", "myapp"])
40
+
41
+ assert (tmp_path / "myapp" / "migrations" / "versions").is_dir()
42
+
43
+
44
+ def test_init_aborts_if_directory_exists(tmp_path):
45
+ runner = CliRunner()
46
+ os.chdir(tmp_path)
47
+ (tmp_path / "myapp").mkdir()
48
+
49
+ result = runner.invoke(cli, ["init", "myapp"])
50
+ assert result.exit_code != 0
51
+ assert "already exists" in result.output or "already exists" in (result.stderr or "")
52
+
53
+
54
+ def test_init_main_py_content(tmp_path):
55
+ runner = CliRunner()
56
+ os.chdir(tmp_path)
57
+ runner.invoke(cli, ["init", "myapp"])
58
+
59
+ content = (tmp_path / "myapp" / "main.py").read_text()
60
+ assert "from bluefox_core import BluefoxSettings, create_bluefox_app" in content
61
+ assert "create_app" in content
62
+ assert 'MODELS_MODULE="models"' in content
63
+
64
+
65
+ def test_init_models_py_content(tmp_path):
66
+ runner = CliRunner()
67
+ os.chdir(tmp_path)
68
+ runner.invoke(cli, ["init", "myapp"])
69
+
70
+ content = (tmp_path / "myapp" / "models.py").read_text()
71
+ assert "BluefoxBase" in content
72
+ assert "Example" in content
73
+
74
+
75
+ def test_init_env_files(tmp_path):
76
+ runner = CliRunner()
77
+ os.chdir(tmp_path)
78
+ runner.invoke(cli, ["init", "myapp"])
79
+
80
+ env_example = (tmp_path / "myapp" / ".env.example").read_text()
81
+ assert "DATABASE_URL=" in env_example
82
+ assert "myapp" in env_example
83
+
84
+ env = (tmp_path / "myapp" / ".env").read_text()
85
+ assert "DEBUG=true" in env
86
+
87
+
88
+ def test_init_makefile_content(tmp_path):
89
+ runner = CliRunner()
90
+ os.chdir(tmp_path)
91
+ runner.invoke(cli, ["init", "myapp"])
92
+
93
+ content = (tmp_path / "myapp" / "Makefile").read_text()
94
+ assert "docker compose" in content
95
+ assert "dev:" in content
96
+ assert "dev-down:" in content
97
+ assert "test:" in content
98
+ assert "migrate:" in content
99
+ assert "migrate-make:" in content
100
+
101
+
102
+ def test_init_dockerfile_content(tmp_path):
103
+ runner = CliRunner()
104
+ os.chdir(tmp_path)
105
+ runner.invoke(cli, ["init", "myapp"])
106
+
107
+ content = (tmp_path / "myapp" / "Dockerfile").read_text()
108
+ assert "FROM ghcr.io/astral-sh/uv" in content
109
+ assert "uvicorn" in content
110
+
111
+
112
+ def test_init_docker_compose_content(tmp_path):
113
+ runner = CliRunner()
114
+ os.chdir(tmp_path)
115
+ runner.invoke(cli, ["init", "myapp"])
116
+
117
+ prod = (tmp_path / "myapp" / "docker-compose.yml").read_text()
118
+ assert "dokploy-network" in prod
119
+ assert "external: true" in prod
120
+ assert "service_completed_successfully" in prod
121
+
122
+ dev = (tmp_path / "myapp" / "docker-compose.dev.yml").read_text()
123
+ assert "myapp" in dev
124
+ assert "pgdata" in dev
125
+
126
+
127
+ def test_init_alembic_config(tmp_path):
128
+ runner = CliRunner()
129
+ os.chdir(tmp_path)
130
+ runner.invoke(cli, ["init", "myapp"])
131
+
132
+ ini = (tmp_path / "myapp" / "alembic.ini").read_text()
133
+ assert "script_location = migrations" in ini
134
+
135
+ env_py = (tmp_path / "myapp" / "migrations" / "env.py").read_text()
136
+ assert "configure_alembic" in env_py
137
+
138
+
139
+ def test_init_test_files(tmp_path):
140
+ runner = CliRunner()
141
+ os.chdir(tmp_path)
142
+ runner.invoke(cli, ["init", "myapp"])
143
+
144
+ conftest = (tmp_path / "myapp" / "tests" / "conftest.py").read_text()
145
+ assert "bluefox_test_setup" in conftest
146
+
147
+ test_health = (tmp_path / "myapp" / "tests" / "test_health.py").read_text()
148
+ assert "/health" in test_health
149
+
150
+
151
+ def test_init_project_name_in_env(tmp_path):
152
+ """Project name should appear in DATABASE_URL and APP_NAME."""
153
+ runner = CliRunner()
154
+ os.chdir(tmp_path)
155
+ runner.invoke(cli, ["init", "cool_project"])
156
+
157
+ env = (tmp_path / "cool_project" / ".env").read_text()
158
+ assert "cool_project" in env
159
+ assert "APP_NAME=cool-project" in env
@@ -0,0 +1,95 @@
1
+ version = 1
2
+ revision = 2
3
+ requires-python = ">=3.12"
4
+
5
+ [[package]]
6
+ name = "bluefox-cli"
7
+ version = "0.1.0"
8
+ source = { editable = "." }
9
+ dependencies = [
10
+ { name = "click" },
11
+ ]
12
+
13
+ [package.dev-dependencies]
14
+ dev = [
15
+ { name = "pytest" },
16
+ ]
17
+
18
+ [package.metadata]
19
+ requires-dist = [{ name = "click", specifier = ">=8.0" }]
20
+
21
+ [package.metadata.requires-dev]
22
+ dev = [{ name = "pytest", specifier = ">=8.0" }]
23
+
24
+ [[package]]
25
+ name = "click"
26
+ version = "8.3.1"
27
+ source = { registry = "https://pypi.org/simple" }
28
+ dependencies = [
29
+ { name = "colorama", marker = "sys_platform == 'win32'" },
30
+ ]
31
+ sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
32
+ wheels = [
33
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
34
+ ]
35
+
36
+ [[package]]
37
+ name = "colorama"
38
+ version = "0.4.6"
39
+ source = { registry = "https://pypi.org/simple" }
40
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
41
+ wheels = [
42
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
43
+ ]
44
+
45
+ [[package]]
46
+ name = "iniconfig"
47
+ version = "2.3.0"
48
+ source = { registry = "https://pypi.org/simple" }
49
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
50
+ wheels = [
51
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
52
+ ]
53
+
54
+ [[package]]
55
+ name = "packaging"
56
+ version = "26.0"
57
+ source = { registry = "https://pypi.org/simple" }
58
+ sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
59
+ wheels = [
60
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
61
+ ]
62
+
63
+ [[package]]
64
+ name = "pluggy"
65
+ version = "1.6.0"
66
+ source = { registry = "https://pypi.org/simple" }
67
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
68
+ wheels = [
69
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
70
+ ]
71
+
72
+ [[package]]
73
+ name = "pygments"
74
+ version = "2.19.2"
75
+ source = { registry = "https://pypi.org/simple" }
76
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
77
+ wheels = [
78
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
79
+ ]
80
+
81
+ [[package]]
82
+ name = "pytest"
83
+ version = "9.0.2"
84
+ source = { registry = "https://pypi.org/simple" }
85
+ dependencies = [
86
+ { name = "colorama", marker = "sys_platform == 'win32'" },
87
+ { name = "iniconfig" },
88
+ { name = "packaging" },
89
+ { name = "pluggy" },
90
+ { name = "pygments" },
91
+ ]
92
+ sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
93
+ wheels = [
94
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
95
+ ]