FastAPI-fastkit 1.4.0__py3-none-any.whl → 1.4.1__py3-none-any.whl

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.
Files changed (24) hide show
  1. fastapi_fastkit/__init__.py +1 -1
  2. fastapi_fastkit/backend/inspection/context.py +4 -0
  3. fastapi_fastkit/backend/inspection/core.py +5 -1
  4. fastapi_fastkit/backend/inspection/docker.py +150 -15
  5. fastapi_fastkit/backend/inspection/lint.py +44 -30
  6. fastapi_fastkit/backend/inspection/smoke.py +67 -13
  7. fastapi_fastkit/backend/inspection/strategies.py +32 -1
  8. fastapi_fastkit/backend/transducer.py +101 -2
  9. fastapi_fastkit/core/settings.py +3 -3
  10. fastapi_fastkit/fastapi_project_template/fastapi-mcp/.env-tpl +1 -1
  11. fastapi_fastkit/fastapi_project_template/fastapi-mcp/README.md-tpl +1 -1
  12. fastapi_fastkit/fastapi_project_template/fastapi-mcp/pyproject.toml-tpl +2 -3
  13. fastapi_fastkit/fastapi_project_template/fastapi-mcp/requirements.txt-tpl +4 -8
  14. fastapi_fastkit/fastapi_project_template/fastapi-mcp/setup.cfg-tpl +2 -3
  15. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/auth.py-tpl +9 -9
  16. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/auth/dependencies.py-tpl +2 -2
  17. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/core/config.py-tpl +1 -1
  18. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/.env-tpl +2 -0
  19. fastapi_fastkit/fragments/auth/jwt.py.j2 +6 -6
  20. {fastapi_fastkit-1.4.0.dist-info → fastapi_fastkit-1.4.1.dist-info}/METADATA +30 -11
  21. {fastapi_fastkit-1.4.0.dist-info → fastapi_fastkit-1.4.1.dist-info}/RECORD +24 -24
  22. {fastapi_fastkit-1.4.0.dist-info → fastapi_fastkit-1.4.1.dist-info}/WHEEL +0 -0
  23. {fastapi_fastkit-1.4.0.dist-info → fastapi_fastkit-1.4.1.dist-info}/entry_points.txt +0 -0
  24. {fastapi_fastkit-1.4.0.dist-info → fastapi_fastkit-1.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -1 +1 @@
1
- __version__ = 'v1.4.0'
1
+ __version__ = 'v1.4.1'
@@ -43,6 +43,10 @@ class InspectionContext:
43
43
  warnings: List[str] = field(default_factory=list)
44
44
  #: Populated by a test strategy so later checks can reuse the environment.
45
45
  venv_path: Optional[str] = None
46
+ #: Populated when a test strategy already exercised the app's HTTP surface
47
+ #: (the Docker strategy probes the running container), so the Smoke Test
48
+ #: step reuses that verdict instead of booting a second server.
49
+ smoke_result: Optional[bool] = None
46
50
 
47
51
  def add_error(self, message: str) -> None:
48
52
  """Record a fatal finding."""
@@ -252,8 +252,12 @@ class TemplateInspector:
252
252
  ("Configuration Consistency", self._check_configuration_consistency),
253
253
  ("FastAPI Implementation", self._check_fastapi_implementation),
254
254
  ("Placeholder Substitution", self._check_no_placeholder_residue),
255
- ("Template Tests", self._test_template),
255
+ # Compile Check is a pure static check: it runs before the (much
256
+ # heavier) Template Tests so a syntax error is reported without
257
+ # waiting for an environment, and so it never observes files a
258
+ # containerised test run left behind.
256
259
  ("Compile Check", self._check_compileall),
260
+ ("Template Tests", self._test_template),
257
261
  ("Type Check", self._check_mypy),
258
262
  ("Smoke Test", self._check_smoke_test),
259
263
  ("Dependency Freshness", self._check_dependency_freshness),
@@ -7,6 +7,7 @@
7
7
  # @author bnbong
8
8
  # --------------------------------------------------------------------------
9
9
  import json
10
+ import os
10
11
  import subprocess
11
12
  import time
12
13
  from typing import Any, Dict, List, Optional, Sequence
@@ -14,6 +15,12 @@ from typing import Any, Dict, List, Optional, Sequence
14
15
  from fastapi_fastkit.utils.logging import debug_log
15
16
 
16
17
  COMPOSE_COMMAND = "docker-compose"
18
+ #: Fallback for hosts that only ship the ``docker compose`` CLI plugin.
19
+ COMPOSE_PLUGIN_COMMAND = ["docker", "compose"]
20
+ #: Resolved compose invocation, switched by :meth:`DockerCompose.is_available`.
21
+ _compose_prefix: List[str] = [COMPOSE_COMMAND]
22
+ #: Throwaway image used to give bind-mounted files back to the host user.
23
+ RECLAIM_IMAGE = "alpine:3.20"
17
24
  SHORT_TIMEOUT = 10
18
25
  STATUS_TIMEOUT = 30
19
26
  CLEANUP_TIMEOUT = 60
@@ -43,27 +50,49 @@ class DockerCompose:
43
50
  debug_log(f"Command {' '.join(args)} failed: {e}", "warning")
44
51
  return None
45
52
 
53
+ @staticmethod
54
+ def compose_command() -> List[str]:
55
+ """Return the compose invocation resolved by :meth:`is_available`."""
56
+ return list(_compose_prefix)
57
+
46
58
  def _compose(
47
59
  self, args: Sequence[str], timeout: int
48
60
  ) -> Optional[subprocess.CompletedProcess[str]]:
49
61
  """Run a docker-compose subcommand against the configured compose file."""
50
62
  return self._run(
51
- [COMPOSE_COMMAND, "-f", self.compose_file, *args], timeout=timeout
63
+ [*self.compose_command(), "-f", self.compose_file, *args], timeout=timeout
52
64
  )
53
65
 
66
+ @staticmethod
67
+ def _probe(command: Sequence[str]) -> bool:
68
+ """Return whether ``command`` exits successfully."""
69
+ try:
70
+ result = subprocess.run(
71
+ list(command), capture_output=True, text=True, timeout=SHORT_TIMEOUT
72
+ )
73
+ except (subprocess.TimeoutExpired, OSError):
74
+ return False
75
+ return result.returncode == 0
76
+
54
77
  @staticmethod
55
78
  def is_available() -> bool:
56
- """Check that both docker and docker-compose respond."""
57
- for command in (["docker", "--version"], [COMPOSE_COMMAND, "--version"]):
58
- try:
59
- result = subprocess.run(
60
- command, capture_output=True, text=True, timeout=SHORT_TIMEOUT
61
- )
62
- except (subprocess.TimeoutExpired, OSError):
63
- return False
64
- if result.returncode != 0:
65
- return False
66
- return True
79
+ """Check that docker responds and a compose implementation exists.
80
+
81
+ ``docker-compose`` stays the preferred command; hosts that only ship
82
+ the ``docker compose`` CLI plugin fall back to it.
83
+ """
84
+ global _compose_prefix
85
+
86
+ if not DockerCompose._probe(["docker", "--version"]):
87
+ return False
88
+ if DockerCompose._probe([COMPOSE_COMMAND, "--version"]):
89
+ _compose_prefix = [COMPOSE_COMMAND]
90
+ return True
91
+ if DockerCompose._probe([*COMPOSE_PLUGIN_COMMAND, "version"]):
92
+ debug_log("Falling back to the 'docker compose' CLI plugin", "info")
93
+ _compose_prefix = list(COMPOSE_PLUGIN_COMMAND)
94
+ return True
95
+ return False
67
96
 
68
97
  def _services(self, timeout: int) -> List[Dict[str, Any]]:
69
98
  """Return the parsed ``docker-compose ps --format json`` entries."""
@@ -71,8 +100,34 @@ class DockerCompose:
71
100
  if result is None or result.returncode != 0:
72
101
  return []
73
102
 
103
+ # Compose sometimes interleaves ``time="..." level=warning`` log lines
104
+ # with the JSON payload, so only JSON-looking lines are considered.
105
+ payload = "\n".join(
106
+ line
107
+ for line in result.stdout.splitlines()
108
+ if line.strip().startswith(("[", "{"))
109
+ ).strip()
110
+ if not payload:
111
+ return []
112
+
113
+ # Compose < v2.21 prints a single JSON array, newer versions print one
114
+ # JSON object per line (NDJSON).
115
+ try:
116
+ document = json.loads(payload)
117
+ except json.JSONDecodeError:
118
+ return DockerCompose._parse_json_lines(payload)
119
+
120
+ if isinstance(document, dict):
121
+ return [document]
122
+ if isinstance(document, list):
123
+ return [entry for entry in document if isinstance(entry, dict)]
124
+ return []
125
+
126
+ @staticmethod
127
+ def _parse_json_lines(payload: str) -> List[Dict[str, Any]]:
128
+ """Parse NDJSON output, skipping malformed or non-object lines."""
74
129
  services: List[Dict[str, Any]] = []
75
- for line in result.stdout.strip().split("\n"):
130
+ for line in payload.split("\n"):
76
131
  if not line.strip():
77
132
  continue
78
133
  try:
@@ -109,7 +164,7 @@ class DockerCompose:
109
164
  """Build and start the compose services in the background."""
110
165
  result = self._compose(["up", "-d", "--build"], timeout=timeout)
111
166
  if result is None:
112
- raise subprocess.TimeoutExpired(COMPOSE_COMMAND, timeout)
167
+ raise subprocess.TimeoutExpired(" ".join(self.compose_command()), timeout)
113
168
  return result
114
169
 
115
170
  def wait_until_healthy(self, timeout: int) -> None:
@@ -166,6 +221,50 @@ class DockerCompose:
166
221
  debug_log("All required services are running", "info")
167
222
  return None
168
223
 
224
+ def published_port(self, service_hint: str = "app") -> Optional[int]:
225
+ """Return the host port the app container publishes, if any.
226
+
227
+ ``docker-compose ps --format json`` reports a ``Publishers`` list per
228
+ service; an entry with a non-zero ``PublishedPort`` is a port bound on
229
+ the host, which is what a smoke test can reach. Services whose name
230
+ contains ``service_hint`` are preferred, so a database that happens to
231
+ publish a port is never mistaken for the application.
232
+ """
233
+ services = self._services(timeout=STATUS_TIMEOUT)
234
+ if not services:
235
+ return None
236
+
237
+ def _matches(service: Dict[str, Any]) -> bool:
238
+ name = f"{service.get('Service', '')} {service.get('Name', '')}"
239
+ return service_hint in name
240
+
241
+ for candidate in (
242
+ [entry for entry in services if _matches(entry)],
243
+ services,
244
+ ):
245
+ for service in candidate:
246
+ port = self._first_published_port(service)
247
+ if port is not None:
248
+ return port
249
+ return None
250
+
251
+ @staticmethod
252
+ def _first_published_port(service: Dict[str, Any]) -> Optional[int]:
253
+ """Extract the first host-bound port from one ``ps`` entry."""
254
+ publishers = service.get("Publishers")
255
+ if not isinstance(publishers, list):
256
+ return None
257
+ for publisher in publishers:
258
+ if not isinstance(publisher, dict):
259
+ continue
260
+ try:
261
+ port = int(publisher.get("PublishedPort", 0))
262
+ except (TypeError, ValueError):
263
+ continue
264
+ if port > 0:
265
+ return port
266
+ return None
267
+
169
268
  def exec_tests(
170
269
  self, use_test_script: bool
171
270
  ) -> Optional[subprocess.CompletedProcess[str]]:
@@ -176,10 +275,46 @@ class DockerCompose:
176
275
  command = ["exec", "-T", "app", "python", "-m", "pytest", "tests/", "-v"]
177
276
  return self._compose(command, timeout=TEST_TIMEOUT)
178
277
 
278
+ def reclaim_bind_mount_ownership(self) -> None:
279
+ """Give the bind-mounted project directory back to the host user.
280
+
281
+ Containers run as root, so a test run inside the stack leaves
282
+ root-owned artefacts (``__pycache__``, ``.pytest_cache``, ...) in the
283
+ mounted project. Later host-side steps and the temp directory cleanup
284
+ cannot touch those and fail with a PermissionError, so the ownership
285
+ is reset from inside a throwaway container. Best effort: a failure
286
+ here is never worth failing an inspection over.
287
+ """
288
+ if not hasattr(os, "getuid"): # pragma: no cover - Windows hosts
289
+ return
290
+
291
+ uid, gid = os.getuid(), os.getgid()
292
+ if uid == 0:
293
+ return
294
+
295
+ debug_log("Reclaiming ownership of bind-mounted project files", "info")
296
+ self._run(
297
+ [
298
+ "docker",
299
+ "run",
300
+ "--rm",
301
+ "-v",
302
+ f"{self.project_dir}:/mnt",
303
+ RECLAIM_IMAGE,
304
+ "chown",
305
+ "-R",
306
+ f"{uid}:{gid}",
307
+ "/mnt",
308
+ ],
309
+ timeout=CLEANUP_TIMEOUT,
310
+ )
311
+
179
312
  def cleanup(self) -> None:
180
313
  """Tear down services and volumes, ignoring any failure."""
181
314
  debug_log("Cleaning up Docker services", "info")
315
+ self.reclaim_bind_mount_ownership()
182
316
  self._run(
183
- [COMPOSE_COMMAND, "down", "-v", "--remove-orphans"], timeout=CLEANUP_TIMEOUT
317
+ [*self.compose_command(), "down", "-v", "--remove-orphans"],
318
+ timeout=CLEANUP_TIMEOUT,
184
319
  )
185
320
  self._run(["docker", "system", "prune", "-f"], timeout=STATUS_TIMEOUT)
@@ -1,12 +1,17 @@
1
1
  # --------------------------------------------------------------------------
2
2
  # Static analysis of the generated project.
3
3
  #
4
- # ``compileall`` is mandatory - a template that cannot be byte-compiled is
5
- # broken beyond argument. ``mypy`` is opt-in because it needs the template's
6
- # own dependencies installed and is far slower.
4
+ # The compile check is mandatory - a template that cannot be compiled is
5
+ # broken beyond argument. It is performed in-process with ``compile()`` and
6
+ # deliberately writes nothing to disk: a Docker test run beforehand can leave
7
+ # root-owned ``__pycache__`` directories in the bind-mounted project, and
8
+ # ``compileall`` would then fail with a PermissionError that says nothing
9
+ # about the template. ``mypy`` is opt-in because it needs the template's own
10
+ # dependencies installed and is far slower.
7
11
  #
8
12
  # @author bnbong
9
13
  # --------------------------------------------------------------------------
14
+ import os
10
15
  import subprocess
11
16
  import sys
12
17
  from typing import List
@@ -15,43 +20,52 @@ from fastapi_fastkit.utils.logging import debug_log
15
20
 
16
21
  from .context import InspectionContext
17
22
 
18
- COMPILE_TIMEOUT = 120
19
23
  MYPY_TIMEOUT = 300
20
24
 
25
+ #: Directories never worth compiling - third party code and build artefacts.
26
+ EXCLUDED_DIRS = {".venv", "venv", "__pycache__", "node_modules"}
27
+
21
28
 
22
29
  def _interpreter(ctx: InspectionContext) -> str:
23
30
  """Prefer the inspection venv interpreter, falling back to the host one."""
24
31
  return ctx.python_executable() or sys.executable
25
32
 
26
33
 
34
+ def _iter_python_files(root: str) -> List[str]:
35
+ """Collect every ``.py`` file below ``root``, skipping excluded directories."""
36
+ python_files: List[str] = []
37
+ for dirpath, dirnames, filenames in os.walk(root):
38
+ dirnames[:] = [name for name in dirnames if name not in EXCLUDED_DIRS]
39
+ for file_name in sorted(filenames):
40
+ if file_name.endswith(".py"):
41
+ python_files.append(os.path.join(dirpath, file_name))
42
+ return python_files
43
+
44
+
27
45
  def check_compileall(ctx: InspectionContext) -> bool:
28
- """Byte-compile every Python file of the generated project."""
29
- command: List[str] = [
30
- _interpreter(ctx),
31
- "-m",
32
- "compileall",
33
- "-q",
34
- "-x",
35
- r"(\.venv|venv|node_modules)",
36
- ctx.temp_dir,
37
- ]
38
- try:
39
- result = subprocess.run(
40
- command,
41
- cwd=ctx.temp_dir,
42
- capture_output=True,
43
- text=True,
44
- timeout=COMPILE_TIMEOUT,
45
- )
46
- except subprocess.TimeoutExpired:
47
- ctx.add_error("compileall timed out")
48
- return False
49
- except OSError as e:
50
- ctx.add_error(f"Failed to run compileall: {e}")
51
- return False
46
+ """Compile every Python file of the generated project, in memory.
52
47
 
53
- if result.returncode != 0:
54
- detail = (result.stderr or result.stdout or "").strip()
48
+ Nothing is written to disk - no ``__pycache__`` is created - so the check
49
+ stays a pure syntax gate that cannot trip over file ownership left behind
50
+ by a containerised test run.
51
+ """
52
+ failures: List[str] = []
53
+
54
+ for path in _iter_python_files(ctx.temp_dir):
55
+ try:
56
+ with open(path, "rb") as f:
57
+ source = f.read()
58
+ except OSError as e:
59
+ failures.append(f"{path}: could not be read ({e})")
60
+ continue
61
+
62
+ try:
63
+ compile(source, path, "exec", dont_inherit=True)
64
+ except (SyntaxError, ValueError) as e:
65
+ failures.append(f"{path}: {e}")
66
+
67
+ if failures:
68
+ detail = "\n".join(failures)
55
69
  ctx.add_error(f"Generated project failed to compile:\n{detail}")
56
70
  return False
57
71
 
@@ -115,12 +115,17 @@ def _probe(url: str, timeout: int = 5) -> Optional[int]:
115
115
 
116
116
 
117
117
  def _wait_for_server(
118
- process: "subprocess.Popen[bytes]", base_url: str, timeout: int
118
+ process: Optional["subprocess.Popen[bytes]"], base_url: str, timeout: int
119
119
  ) -> Tuple[bool, str]:
120
- """Poll ``/docs`` until the server answers, the process dies or time runs out."""
120
+ """Poll ``/docs`` until the server answers, the process dies or time runs out.
121
+
122
+ ``process`` is ``None`` when the server is not ours to watch - a container
123
+ started by the Docker strategy, for one - in which case only the timeout
124
+ bounds the wait.
125
+ """
121
126
  deadline = time.time() + timeout
122
127
  while time.time() < deadline:
123
- if process.poll() is not None:
128
+ if process is not None and process.poll() is not None:
124
129
  return False, "server process exited before becoming reachable"
125
130
  status = _probe(f"{base_url}/docs")
126
131
  if status is not None:
@@ -228,14 +233,70 @@ def _read_log_tail(log_file: IO[bytes]) -> str:
228
233
  return ""
229
234
 
230
235
 
236
+ def _check_health_endpoint(ctx: InspectionContext, base_url: str) -> Optional[str]:
237
+ """Probe ``/health``; return a failure message, or ``None`` when acceptable.
238
+
239
+ A template without a ``/health`` route answers 404, which is fine; an
240
+ unreachable endpoint is only worth a warning, but a route that exists and
241
+ answers with anything other than 200 is a real failure.
242
+ """
243
+ health_status = _probe(f"{base_url}/health")
244
+ if health_status is None:
245
+ ctx.add_warning("Smoke test: /health did not respond")
246
+ elif health_status == 404:
247
+ debug_log("Template exposes no /health endpoint, skipping", "info")
248
+ elif health_status != 200:
249
+ return (
250
+ f"Smoke test failed: /health returned HTTP {health_status} (expected 200)"
251
+ )
252
+ return None
253
+
254
+
255
+ def run_http_smoke(ctx: InspectionContext, base_url: str) -> bool:
256
+ """Verify the HTTP surface of a server someone else already started.
257
+
258
+ Used by the Docker strategy, which has the real application running in a
259
+ container: probing its published port is more honest than booting a second
260
+ copy on the host, and templates that require Docker never get a host venv
261
+ to boot one with in the first place.
262
+ """
263
+ debug_log(f"Running smoke test against {base_url}", "info")
264
+ reachable, reason = _wait_for_server(None, base_url, ctx.options.smoke_timeout)
265
+ if not reachable:
266
+ ctx.add_error(f"Smoke test failed: {reason}")
267
+ return False
268
+
269
+ failure = _check_health_endpoint(ctx, base_url)
270
+ if failure:
271
+ ctx.add_error(failure)
272
+ return False
273
+
274
+ debug_log("Smoke test passed", "info")
275
+ return True
276
+
277
+
278
+ def _requires_docker(ctx: InspectionContext) -> bool:
279
+ """Whether the template declares that it can only run under Docker."""
280
+ return bool((ctx.template_config or {}).get("requires_docker", False))
281
+
282
+
231
283
  def check_smoke_test(ctx: InspectionContext) -> bool:
232
284
  """Boot the generated project and verify its HTTP surface."""
233
285
  if not ctx.options.run_smoke_test:
234
286
  debug_log("Smoke test disabled", "info")
235
287
  return True
236
288
 
289
+ if ctx.smoke_result is not None:
290
+ debug_log("Reusing the smoke test result recorded while testing", "info")
291
+ return ctx.smoke_result
292
+
237
293
  python_executable = ctx.python_executable()
238
294
  if not python_executable or not os.path.exists(python_executable):
295
+ if _requires_docker(ctx):
296
+ ctx.add_warning(
297
+ "smoke test skipped: Docker template without published port"
298
+ )
299
+ return True
239
300
  ctx.add_error(
240
301
  "Smoke test requires an installed environment, but no virtual "
241
302
  "environment was prepared for the generated project"
@@ -306,16 +367,9 @@ def check_smoke_test(ctx: InspectionContext) -> bool:
306
367
  ctx.add_error(last_failure)
307
368
  return False
308
369
 
309
- health_status = _probe(f"{base_url}/health")
310
- if health_status is None:
311
- ctx.add_warning("Smoke test: /health did not respond")
312
- elif health_status == 404:
313
- debug_log("Template exposes no /health endpoint, skipping", "info")
314
- elif health_status != 200:
315
- ctx.add_error(
316
- f"Smoke test failed: /health returned HTTP {health_status} "
317
- "(expected 200)\n" + _read_log_tail(log_file)
318
- )
370
+ failure = _check_health_endpoint(ctx, base_url)
371
+ if failure:
372
+ ctx.add_error(f"{failure}\n{_read_log_tail(log_file)}".rstrip())
319
373
  return False
320
374
  finally:
321
375
  _terminate(process)
@@ -16,6 +16,7 @@ from typing import Any, Dict, List
16
16
  from fastapi_fastkit.backend.main import create_venv, install_dependencies_with_manager
17
17
  from fastapi_fastkit.utils.logging import debug_log
18
18
 
19
+ from . import smoke
19
20
  from .context import InspectionContext
20
21
  from .docker import DockerCompose
21
22
  from .fsutils import fix_all_script_line_endings, fix_script_line_endings
@@ -278,7 +279,15 @@ class DockerStrategy(TestStrategy):
278
279
  self.ctx.add_error(verification_error)
279
280
  return False
280
281
 
281
- return self._run_tests(compose)
282
+ if not self._run_tests(compose):
283
+ return False
284
+
285
+ # The application is running right here, in a container with a
286
+ # published port: probing that is the only way a Docker-only
287
+ # template gets a smoke test, since no host venv is ever built for
288
+ # it. Done before the ``finally`` below tears the stack down.
289
+ self._run_smoke_test(compose)
290
+ return True
282
291
  except subprocess.TimeoutExpired:
283
292
  self.ctx.add_error("Docker Compose setup timed out")
284
293
  return False
@@ -307,6 +316,28 @@ class DockerStrategy(TestStrategy):
307
316
  debug_log("Docker tests passed successfully", "info")
308
317
  return True
309
318
 
319
+ def _run_smoke_test(self, compose: DockerCompose) -> None:
320
+ """Probe the running container's HTTP surface and record the verdict.
321
+
322
+ The result lands on the context so the pipeline's Smoke Test step
323
+ reuses it instead of trying (and failing) to boot the project from a
324
+ virtual environment that a Docker run never creates.
325
+ """
326
+ if not self.ctx.options.run_smoke_test:
327
+ return
328
+
329
+ port = compose.published_port()
330
+ if port is None:
331
+ self.ctx.add_warning(
332
+ "smoke test skipped: Docker template without published port"
333
+ )
334
+ self.ctx.smoke_result = True
335
+ return
336
+
337
+ self.ctx.smoke_result = smoke.run_http_smoke(
338
+ self.ctx, f"http://127.0.0.1:{port}"
339
+ )
340
+
310
341
 
311
342
  def select_fallback_strategy(ctx: InspectionContext) -> TestStrategy:
312
343
  """Pick the fallback strategy, or the standard one when none is configured."""
@@ -22,6 +22,104 @@ logger = get_logger(__name__)
22
22
  #: here are the *converted* names (the ``-tpl`` marker already stripped).
23
23
  TEMPLATE_ONLY_FILES = frozenset({"template-config.yml"})
24
24
 
25
+ #: Extensions of files that are known to be text and therefore safe to rewrite
26
+ #: with Unix line endings. Anything outside this list is copied byte for byte,
27
+ #: so an image or an archive shipped inside a template survives untouched.
28
+ TEXT_FILE_EXTENSIONS = frozenset(
29
+ {
30
+ ".bash",
31
+ ".cfg",
32
+ ".css",
33
+ ".env",
34
+ ".html",
35
+ ".ini",
36
+ ".js",
37
+ ".json",
38
+ ".mako",
39
+ ".md",
40
+ ".py",
41
+ ".rst",
42
+ ".sh",
43
+ ".sql",
44
+ ".toml",
45
+ ".ts",
46
+ ".txt",
47
+ ".yaml",
48
+ ".yml",
49
+ }
50
+ )
51
+
52
+ #: Extension-less file names that are text as well.
53
+ TEXT_FILE_NAMES = frozenset(
54
+ {
55
+ ".dockerignore",
56
+ ".env",
57
+ ".gitignore",
58
+ "CHANGELOG",
59
+ "Dockerfile",
60
+ "LICENSE",
61
+ "Makefile",
62
+ "Procfile",
63
+ "README",
64
+ }
65
+ )
66
+
67
+ #: How much of a file is sampled when looking for a NUL byte. A NUL in the
68
+ #: first chunk is the same heuristic Git uses to call a blob binary.
69
+ BINARY_SNIFF_BYTES = 8192
70
+
71
+
72
+ def _looks_like_text_file(file_path: str, file_name: str) -> bool:
73
+ """
74
+ Decide whether a copied file may have its line endings normalised.
75
+
76
+ The check is deliberately conservative: the name has to be on the text
77
+ whitelist *and* the content must carry no NUL byte, so a mislabelled
78
+ binary is left alone rather than corrupted.
79
+
80
+ :param file_path: Path of the file to inspect
81
+ :param file_name: File name used for the extension/name whitelist
82
+ :return: True when the file is safe to rewrite as text
83
+ """
84
+ _, extension = os.path.splitext(file_name)
85
+ if (
86
+ extension.lower() not in TEXT_FILE_EXTENSIONS
87
+ and file_name not in TEXT_FILE_NAMES
88
+ ):
89
+ return False
90
+
91
+ try:
92
+ with open(file_path, "rb") as f:
93
+ return b"\x00" not in f.read(BINARY_SNIFF_BYTES)
94
+ except OSError as e:
95
+ debug_log(f"Could not sniff {file_path} for binary content: {e}", "warning")
96
+ return False
97
+
98
+
99
+ def _normalize_line_endings(file_path: str, file_name: str) -> None:
100
+ """
101
+ Rewrite a copied text file with Unix line endings.
102
+
103
+ Templates checked out on Windows (or with ``core.autocrlf=true``) carry
104
+ CRLF, which makes a generated project's shell scripts unusable inside a
105
+ Linux container: ``env: 'bash\r': No such file or directory``. The rewrite
106
+ happens in place through :func:`fix_script_line_endings`, which truncates
107
+ rather than recreates the file and therefore keeps the executable bit
108
+ ``shutil.copy2`` just carried over.
109
+
110
+ :param file_path: Path of the copied file
111
+ :param file_name: File name used for the text/binary decision
112
+ """
113
+ if not _looks_like_text_file(file_path, file_name):
114
+ return
115
+
116
+ # Imported lazily: ``fastapi_fastkit.backend.inspection`` pulls in the
117
+ # scaffolder, which imports this module, so a top-level import would be
118
+ # circular.
119
+ from fastapi_fastkit.backend.inspection.fsutils import fix_script_line_endings
120
+
121
+ fix_script_line_endings(file_path)
122
+
25
123
 
26
124
  def copy_and_convert_template(
27
125
  template_dir: str, target_dir: str, project_name: str = ""
@@ -130,6 +228,7 @@ def _copy_template_file(
130
228
 
131
229
  try:
132
230
  shutil.copy2(src_file, dst_file)
231
+ _normalize_line_endings(dst_file, dst_file_name)
133
232
  debug_log(f"Copied {src_file} to {dst_file}", "debug")
134
233
  return dst_file
135
234
 
@@ -223,8 +322,8 @@ def _write_target_file(target_file: str, content: str, source_file: str) -> bool
223
322
  target_dir = os.path.dirname(target_file)
224
323
  os.makedirs(target_dir, exist_ok=True)
225
324
 
226
- with open(target_file, "w", encoding="utf-8") as f:
227
- f.write(content)
325
+ with open(target_file, "w", encoding="utf-8", newline="\n") as f:
326
+ f.write(content.replace("\r\n", "\n").replace("\r", "\n"))
228
327
 
229
328
  debug_log(
230
329
  f"Successfully copied template file from {source_file} to {target_file}",
@@ -257,13 +257,13 @@ class FastkitConfig:
257
257
  DatabaseChoice.NONE: [],
258
258
  },
259
259
  FeatureAxis.AUTHENTICATION: {
260
- AuthChoice.JWT: ["python-jose[cryptography]", "passlib[bcrypt]"],
260
+ AuthChoice.JWT: ["pyjwt[crypto]", "pwdlib[argon2]"],
261
261
  # SessionMiddleware (used by the OAuth2 login flow) needs itsdangerous.
262
262
  AuthChoice.OAUTH2: ["authlib", "itsdangerous", "httpx"],
263
263
  AuthChoice.FASTAPI_USERS: [
264
264
  "fastapi-users[sqlalchemy]",
265
- "python-jose[cryptography]",
266
- "passlib[bcrypt]",
265
+ "pyjwt[crypto]",
266
+ "pwdlib[argon2]",
267
267
  ],
268
268
  AuthChoice.SESSION: ["itsdangerous"],
269
269
  AuthChoice.NONE: [],
@@ -21,7 +21,7 @@ MCP_TITLE=FastAPI MCP Server
21
21
  MCP_DESCRIPTION=FastAPI endpoints exposed as MCP tools
22
22
 
23
23
  # Authentication Settings
24
- SECRET_KEY=changethis
24
+ SECRET_KEY=changethis-please-use-openssl-rand-hex-32
25
25
  ALGORITHM=HS256
26
26
  ACCESS_TOKEN_EXPIRE_MINUTES=30
27
27
 
@@ -75,7 +75,7 @@ MCP_TITLE=FastAPI MCP Server
75
75
  MCP_DESCRIPTION=FastAPI endpoints exposed as MCP tools
76
76
 
77
77
  # Authentication Settings
78
- SECRET_KEY=changethis
78
+ SECRET_KEY=changethis-please-use-openssl-rand-hex-32
79
79
  ALGORITHM=HS256
80
80
  ACCESS_TOKEN_EXPIRE_MINUTES=30
81
81
 
@@ -17,9 +17,8 @@ dependencies = [
17
17
  "python-dotenv>=1.2.3",
18
18
  "fastapi-mcp>=0.3.7,<0.4.0",
19
19
  "mcp>=1.29.1,<2.0.0",
20
- "bcrypt>=4.0.1,<4.1.0",
21
- "passlib>=1.7.4",
22
- "python-jose>=3.5.0",
20
+ "pwdlib[argon2]>=0.3.1",
21
+ "PyJWT[crypto]>=2.13.0",
23
22
  "python-multipart>=0.0.32",
24
23
  ]
25
24
 
@@ -1,9 +1,10 @@
1
1
  annotated-doc==0.0.5
2
2
  annotated-types==0.8.0
3
3
  anyio==4.15.0
4
+ argon2-cffi==25.1.0
5
+ argon2-cffi-bindings==26.1.0
4
6
  ast-serialize==0.9.0
5
7
  attrs==26.1.0
6
- bcrypt==4.0.1
7
8
  black==26.5.1
8
9
  certifi==2026.7.22
9
10
  cffi==2.1.1
@@ -11,7 +12,6 @@ charset-normalizer==3.5.1
11
12
  click==8.5.0
12
13
  coverage==7.16.0
13
14
  cryptography==50.0.1
14
- ecdsa==0.19.2
15
15
  fastapi==0.141.1
16
16
  fastapi-mcp==0.3.7
17
17
  h11==0.16.0
@@ -31,21 +31,19 @@ mdurl==0.1.2
31
31
  mypy==2.3.1
32
32
  mypy-extensions==1.1.0
33
33
  packaging==26.3
34
- passlib==1.7.4
35
34
  pathspec==1.1.1
36
35
  platformdirs==4.11.7
37
36
  pluggy==1.6.0
38
- pyasn1==0.6.4
37
+ pwdlib==0.3.1
39
38
  pycparser==3.0
40
39
  pydantic==2.13.5
41
40
  pydantic-core==2.46.5
42
41
  pydantic-settings==2.15.0
43
42
  pygments==2.21.0
44
- pyjwt==2.13.0
43
+ PyJWT==2.13.0
45
44
  pytest==9.1.1
46
45
  pytest-cov==7.1.0
47
46
  python-dotenv==1.2.3
48
- python-jose==3.5.0
49
47
  python-multipart==0.0.32
50
48
  pytokens==0.4.1
51
49
  PyYAML==6.0.3
@@ -53,9 +51,7 @@ referencing==0.37.0
53
51
  requests==2.34.2
54
52
  rich==15.0.0
55
53
  rpds-py==2026.6.3
56
- rsa==4.9.1
57
54
  shellingham==1.5.4
58
- six==1.17.0
59
55
  SQLAlchemy==2.0.52
60
56
  sse-starlette==3.4.10
61
57
  starlette==1.6.0
@@ -29,9 +29,8 @@ install_requires =
29
29
  pydantic>=2.10.6
30
30
  pydantic-settings>=2.7.1
31
31
  python-dotenv>=1.0.1
32
- python-jose>=3.3.0
33
- passlib>=1.7.4
34
- bcrypt>=4.1.2
32
+ PyJWT[crypto]>=2.10.0
33
+ pwdlib[argon2]>=0.3.1
35
34
  python-multipart>=0.0.17
36
35
 
37
36
  [options.extras_require]
@@ -3,10 +3,10 @@ Authentication-related API endpoints.
3
3
  """
4
4
  from datetime import datetime, timedelta, timezone
5
5
 
6
+ import jwt
6
7
  from fastapi import APIRouter, Depends, HTTPException, status
7
8
  from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
8
- from jose import jwt
9
- from passlib.context import CryptContext
9
+ from pwdlib import PasswordHash
10
10
 
11
11
  from src.auth.dependencies import get_current_active_user
12
12
  from src.core.config import settings
@@ -14,8 +14,8 @@ from src.schemas.items import AuthToken, UserInfo, UserLogin
14
14
 
15
15
  router = APIRouter()
16
16
 
17
- # Password hashing
18
- pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
17
+ # Password hashing — argon2id, the recommended default for new applications.
18
+ password_hash = PasswordHash.recommended()
19
19
 
20
20
  # OAuth2 scheme
21
21
  oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@@ -25,19 +25,19 @@ mock_users = {
25
25
  "user1": {
26
26
  "user_id": "user1",
27
27
  "username": "user1",
28
- "hashed_password": pwd_context.hash("password123"),
28
+ "hashed_password": password_hash.hash("password123"),
29
29
  "active": True,
30
30
  },
31
31
  "user2": {
32
32
  "user_id": "user2",
33
33
  "username": "user2",
34
- "hashed_password": pwd_context.hash("password456"),
34
+ "hashed_password": password_hash.hash("password456"),
35
35
  "active": True,
36
36
  },
37
37
  "admin": {
38
38
  "user_id": "admin",
39
39
  "username": "admin",
40
- "hashed_password": pwd_context.hash("admin123"),
40
+ "hashed_password": password_hash.hash("admin123"),
41
41
  "active": True,
42
42
  },
43
43
  }
@@ -45,12 +45,12 @@ mock_users = {
45
45
 
46
46
  def verify_password(plain_password: str, hashed_password: str) -> bool:
47
47
  """Verify a password against its hash."""
48
- return pwd_context.verify(plain_password, hashed_password)
48
+ return password_hash.verify(plain_password, hashed_password)
49
49
 
50
50
 
51
51
  def get_password_hash(password: str) -> str:
52
52
  """Generate password hash."""
53
- return pwd_context.hash(password)
53
+ return password_hash.hash(password)
54
54
 
55
55
 
56
56
  def authenticate_user(username: str, password: str) -> dict | None:
@@ -2,9 +2,9 @@
2
2
  Authentication dependencies for API and MCP endpoints.
3
3
  """
4
4
 
5
+ import jwt
5
6
  from fastapi import Depends, HTTPException, status
6
7
  from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
7
- from jose import JWTError, jwt
8
8
 
9
9
  from src.core.config import settings
10
10
 
@@ -16,7 +16,7 @@ def verify_token(token: str) -> dict:
16
16
  try:
17
17
  payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
18
18
  return payload
19
- except JWTError:
19
+ except jwt.PyJWTError:
20
20
  raise HTTPException(
21
21
  status_code=status.HTTP_401_UNAUTHORIZED,
22
22
  detail="Could not validate credentials",
@@ -28,7 +28,7 @@ class Settings(BaseSettings):
28
28
  MCP_DESCRIPTION: str = "FastAPI endpoints exposed as MCP tools"
29
29
 
30
30
  # Authentication settings
31
- SECRET_KEY: str = "your-secret-key-here"
31
+ SECRET_KEY: str = "your-secret-key-here-change-in-production"
32
32
  ALGORITHM: str = "HS256"
33
33
  ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
34
34
 
@@ -1,4 +1,6 @@
1
1
  # Backend
2
+ # Must be one of the values accepted by src/core/config.py: development | production
3
+ ENVIRONMENT=development
2
4
  SECRET_KEY=changethis
3
5
 
4
6
  # Postgres
@@ -1,8 +1,8 @@
1
1
  {% include "header.j2" %}
2
2
  from datetime import UTC, datetime, timedelta
3
3
 
4
- from jose import jwt
5
- from passlib.context import CryptContext
4
+ import jwt
5
+ from pwdlib import PasswordHash
6
6
  from pydantic_settings import BaseSettings, SettingsConfigDict
7
7
 
8
8
 
@@ -18,18 +18,18 @@ class AuthSettings(BaseSettings):
18
18
 
19
19
  settings = AuthSettings()
20
20
 
21
- # Password hashing
22
- pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
21
+ # Password hashing — argon2id, the recommended default for new applications.
22
+ password_hash = PasswordHash.recommended()
23
23
 
24
24
 
25
25
  def verify_password(plain_password: str, hashed_password: str) -> bool:
26
26
  """Verify a password against a hash."""
27
- return pwd_context.verify(plain_password, hashed_password)
27
+ return password_hash.verify(plain_password, hashed_password)
28
28
 
29
29
 
30
30
  def get_password_hash(password: str) -> str:
31
31
  """Hash a password."""
32
- return pwd_context.hash(password)
32
+ return password_hash.hash(password)
33
33
 
34
34
 
35
35
  def create_access_token(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: FastAPI-fastkit
3
- Version: 1.4.0
3
+ Version: 1.4.1
4
4
  Summary: Fast, easy-to-use starter kit for new users of Python and FastAPI
5
5
  Author-Email: bnbong <bbbong9@gmail.com>
6
6
  License: MIT
@@ -82,7 +82,9 @@ $ pip install FastAPI-fastkit
82
82
  ```console
83
83
  fastkit init [OPTIONS]
84
84
  ```
85
- - What it does: Scaffolds an empty FastAPI project, creates a virtual environment, installs dependencies
85
+ > [!TIP]
86
+ > Scaffolds an empty FastAPI project, creates a virtual environment, installs dependencies
87
+
86
88
  - Key options:
87
89
  - `--project-name`, `--author`, `--author-email`, `--description`
88
90
  - `--package-manager` [pip|uv|pdm|poetry]
@@ -98,7 +100,9 @@ fastkit init [OPTIONS]
98
100
  ```console
99
101
  fastkit init --interactive
100
102
  ```
101
- - What it does: Guided step-by-step project setup with intelligent feature selection
103
+ > [!TIP]
104
+ > Guided step-by-step project setup with intelligent feature selection
105
+
102
106
  - Features:
103
107
  - **Architecture preset**: `minimal` | `single-module` | `classic-layered` | `domain-starter` (default)
104
108
  - **Database selection**: PostgreSQL, MySQL, MongoDB, Redis, SQLite
@@ -114,7 +118,7 @@ fastkit init --interactive
114
118
  - **Deployment**: Docker, docker-compose with auto-generated configs
115
119
  - **Package manager**: pip, uv, pdm, poetry
116
120
  - **Custom packages**: Add your own dependencies
117
- - Auto-generates (varies by preset see [the matrix](https://bnbong.github.io/FastAPI-fastkit/reference/preset-feature-matrix/) for details):
121
+ - Auto-generates (varies by preset - see [the matrix](https://bnbong.github.io/FastAPI-fastkit/reference/preset-feature-matrix/) for details):
118
122
  - `main.py` regenerated from selected features for `minimal` / `single-module`; preserved as-shipped for `classic-layered` / `domain-starter`
119
123
  - Database and authentication configuration files at preset-specific paths
120
124
  - Docker deployment files (`Dockerfile`, `docker-compose.yml`) with the preset's correct uvicorn entrypoint
@@ -127,7 +131,9 @@ fastkit init --interactive
127
131
  ```console
128
132
  fastkit startdemo [TEMPLATE] [OPTIONS]
129
133
  ```
130
- - What it does: Creates a project from a template (e.g., `fastapi-default`) and installs dependencies
134
+ > [!TIP]
135
+ > Creates a project from a template (e.g., `fastapi-default`) and installs dependencies
136
+
131
137
  - Key options:
132
138
  - `--project-name`, `--author`, `--author-email`, `--description`
133
139
  - `--package-manager` [pip|uv|pdm|poetry]
@@ -135,12 +141,6 @@ fastkit startdemo [TEMPLATE] [OPTIONS]
135
141
  - Refuses to run if the target project directory already exists (`--dry-run` excepted)
136
142
  - Tip: List available templates with `fastkit list-templates`
137
143
 
138
- New in v1.4.0: `fastapi-auth-jwt` (JWT auth with refresh rotation),
139
- `fastapi-sqlmodel` (async SQLModel + Alembic + generic CRUD), and
140
- `fastapi-llm-agent` (streaming Claude agent with a tool loop).
141
- `fastapi-dockerized` and `fastapi-async-crud` are deprecated — they still
142
- work, but are no longer recommended starting points.
143
-
144
144
  ### Add a new route
145
145
  ```console
146
146
  fastkit addroute <project_name> <route_name>
@@ -160,6 +160,25 @@ fastkit runserver [OPTIONS]
160
160
  fastkit list-templates
161
161
  ```
162
162
 
163
+ Available templates:
164
+
165
+ | Template | Description | Notes |
166
+ |---|---|---|
167
+ | [`fastapi-default`](src/fastapi_fastkit/fastapi_project_template/fastapi-default/README.md-tpl) | Simple FastAPI project with a classic layered layout | Good first choice |
168
+ | [`fastapi-empty`](src/fastapi_fastkit/fastapi_project_template/fastapi-empty/README.md-tpl) | Minimal FastAPI template | Base for `minimal` preset |
169
+ | [`fastapi-single-module`](src/fastapi_fastkit/fastapi_project_template/fastapi-single-module/README.md-tpl) | Single-file FastAPI app | Base for `single-module` preset |
170
+ | [`fastapi-domain-starter`](src/fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/README.md-tpl) | Domain-oriented, pyproject-first starter for medium-sized APIs | Base for `domain-starter` preset |
171
+ | [`fastapi-auth-jwt`](src/fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/README.md-tpl) | JWT authentication with refresh-token rotation, argon2 hashing and scopes | New in v1.4.0 |
172
+ | [`fastapi-sqlmodel`](src/fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/README.md-tpl) | Async SQLModel + Alembic migrations + generic CRUD with pagination | New in v1.4.0 |
173
+ | [`fastapi-llm-agent`](src/fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/README.md-tpl) | Streaming Claude chat agent (SSE) with a tool-call loop | New in v1.4.0 |
174
+ | [`fastapi-mcp`](src/fastapi_fastkit/fastapi_project_template/fastapi-mcp/README.md-tpl) | FastAPI app exposed as an MCP server | |
175
+ | [`fastapi-psql-orm`](src/fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/README.md-tpl) | Item management API with PostgreSQL, SQLModel, Alembic and docker-compose | Requires Docker |
176
+ | [`fastapi-custom-response`](src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/README.md-tpl) | Item management API with a custom response envelope, error handling and pagination | |
177
+ | [`fastapi-async-crud`](src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud/README.md-tpl) | Async item management API | **Deprecated** — use `fastapi-sqlmodel` |
178
+ | [`fastapi-dockerized`](src/fastapi_fastkit/fastapi_project_template/fastapi-dockerized/README.md-tpl) | Dockerized item management API | **Deprecated** — use `fastapi-default` with the Docker option in `init --interactive` |
179
+
180
+ Deprecated templates still work but are no longer recommended starting points. See [Which starter should I choose?](https://bnbong.github.io/FastAPI-fastkit/user-guide/choosing-a-starter/) for a decision guide.
181
+
163
182
  ### Delete a project
164
183
  ```console
165
184
  fastkit deleteproject <project_name>
@@ -1,22 +1,22 @@
1
- fastapi_fastkit-1.4.0.dist-info/METADATA,sha256=wedGfADPNstm5uSxEkzj-L1R8aN_bT7ssYM3ObJ-b3o,11905
2
- fastapi_fastkit-1.4.0.dist-info/WHEEL,sha256=VP-D4TPS230sME9Z3vb3INXvo1yt0924YRm5AOsk_dE,90
3
- fastapi_fastkit-1.4.0.dist-info/entry_points.txt,sha256=IONmgb7zWPnJWsCOcpF3u1yP6AWnPjrgWIv49mr1DZE,76
4
- fastapi_fastkit-1.4.0.dist-info/licenses/LICENSE,sha256=2a9cYM3Uy8DW-so6zpYaqUafYT1Dznd3OCWCFe5CKNA,1066
5
- fastapi_fastkit/__init__.py,sha256=Fc7Dma-w6Qol1wV5ZlHolbiRE3Am7eHtFGZvbNRu_8E,23
1
+ fastapi_fastkit-1.4.1.dist-info/METADATA,sha256=KRHsuGhz10aD8Nckit_OsoJwMcSiwKbYM9zcfZttGak,14058
2
+ fastapi_fastkit-1.4.1.dist-info/WHEEL,sha256=VP-D4TPS230sME9Z3vb3INXvo1yt0924YRm5AOsk_dE,90
3
+ fastapi_fastkit-1.4.1.dist-info/entry_points.txt,sha256=IONmgb7zWPnJWsCOcpF3u1yP6AWnPjrgWIv49mr1DZE,76
4
+ fastapi_fastkit-1.4.1.dist-info/licenses/LICENSE,sha256=2a9cYM3Uy8DW-so6zpYaqUafYT1Dznd3OCWCFe5CKNA,1066
5
+ fastapi_fastkit/__init__.py,sha256=muhECMM1myXdjnRz3xB2_5q1rOYCUaw2jAkKMCZqrJ8,23
6
6
  fastapi_fastkit/__main__.py,sha256=-FS9yUe4IEgDbJjFC1xso-pFCxRzUJ447j6bYpsBTBM,271
7
7
  fastapi_fastkit/backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  fastapi_fastkit/backend/inspection/__init__.py,sha256=hIw6MIOrt5GI52kQtYWsW1INbuVd64vG_OGU_rvfXXE,1088
9
9
  fastapi_fastkit/backend/inspection/checks.py,sha256=rW-CfDaPXejo1Hvoy7LskOKTdInZMK1wPj4hhl1z380,11946
10
10
  fastapi_fastkit/backend/inspection/consistency.py,sha256=kmW9D4eN2qCpiygezM4JpqFZ7SC7-wUImGt53ej4Vu4,4941
11
- fastapi_fastkit/backend/inspection/context.py,sha256=K5GsITSMsa-Pgl6eOmU1C5BqRcaq0Doc0pNEbt33F1E,2582
12
- fastapi_fastkit/backend/inspection/core.py,sha256=AB1eZLIcmdZWJ96XeO1Cs4pNzMbduWja_do1krrPEHs,13454
13
- fastapi_fastkit/backend/inspection/docker.py,sha256=fI6NvGnIdOh-HOItPRgM8hn6OeaAmZJnzcUSmgTJWIU,7112
11
+ fastapi_fastkit/backend/inspection/context.py,sha256=p0KBW6SaKZ2eHn8xY8zcF6mLOqi-4aHp6SCc9_Jz4yE,2846
12
+ fastapi_fastkit/backend/inspection/core.py,sha256=mg8LqrrhfOl5UZGRPEDIiPo5frTTimzBpytdwsRgTUc,13732
13
+ fastapi_fastkit/backend/inspection/docker.py,sha256=pMMSf7_sP83sq4Ibr4l6SLUjRtfoRloDHh5neGIEFLc,12305
14
14
  fastapi_fastkit/backend/inspection/freshness.py,sha256=CVpoV6IwrRTdSP9leaHD8CJU2OKZs-y_HcHiZ9Nbx7w,5418
15
15
  fastapi_fastkit/backend/inspection/fsutils.py,sha256=Vt_FNVBcJuU7qsR71lgQZrFy2WRqo056y7f5Up-D6Ds,4758
16
- fastapi_fastkit/backend/inspection/lint.py,sha256=Lu9rUQL-26VbqzMf_BA6L0llnQmOpuOaeCxue1XnOVY,2664
16
+ fastapi_fastkit/backend/inspection/lint.py,sha256=BEkrghyDY4Sl5NPsHzDi_3Trcjw4SvMTT3tSV_2DBgU,3591
17
17
  fastapi_fastkit/backend/inspection/report.py,sha256=BYkGkotGYXJIEnlolT1g8TxmZRg422ryEXGU2pLnEpI,1930
18
- fastapi_fastkit/backend/inspection/smoke.py,sha256=hDCv4BtFUs6urjOGE3vto9xh7qBwEQeyvM227z_6xAQ,12187
19
- fastapi_fastkit/backend/inspection/strategies.py,sha256=0rfESk2Vet5_-j3uWAZR86urUQEju9LskKEZVtdip-A,12329
18
+ fastapi_fastkit/backend/inspection/smoke.py,sha256=fkEp9l2R3DhkUpwr8qhGCRiYa6GSDvuiuSZfgHq9uIs,14203
19
+ fastapi_fastkit/backend/inspection/strategies.py,sha256=fyOBAcYk9iYUFNRmDE8VVfPLwMvdodUyj719byvZhF8,13546
20
20
  fastapi_fastkit/backend/inspector.py,sha256=73ZY7PQGQvxbbBwWPxfuudJcxOrLKvWkkzBUcS6YgGc,1002
21
21
  fastapi_fastkit/backend/interactive/__init__.py,sha256=wuCVfyaulyjGiG77X0C9sUwY2zr2S6rAGeCeh_17MVA,1873
22
22
  fastapi_fastkit/backend/interactive/config_builder.py,sha256=LKqmUiBw2-Dk5OPk4Ut8D2eYRQvWAfcNP0LyCzAVykE,6928
@@ -37,11 +37,11 @@ fastapi_fastkit/backend/project_builder/config_schema.py,sha256=Vj4Mmkr35MnoVbST
37
37
  fastapi_fastkit/backend/project_builder/dependency_collector.py,sha256=vj0NYAi3KdvZ5jjgUxFSoP5J5KAHvqIsGdlU5H7xH48,7582
38
38
  fastapi_fastkit/backend/project_builder/preset_layout.py,sha256=E-I8JP3goHW5QwpACBC6LC7XX00c-5BNsPjVlDYOhVQ,16319
39
39
  fastapi_fastkit/backend/scaffolder.py,sha256=dduEeBN1cdwt_V3JxSxSepHEUMuEB0j-8Sq5FU8KnJ0,21702
40
- fastapi_fastkit/backend/transducer.py,sha256=ZWh5D9o-sBPpLoJYm1l_mT_ppcXYdSIlIdJe0iefDBk,8827
40
+ fastapi_fastkit/backend/transducer.py,sha256=t0tHVNUpPgbQcwF6Ms9WhQ8ALl6MR1zL8CVbvKgD2rs,11977
41
41
  fastapi_fastkit/cli.py,sha256=UxwJcLOkCspwVFnNgJQkbYQyTjsrcNdRpjNnRTE3Rgk,35305
42
42
  fastapi_fastkit/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
43
  fastapi_fastkit/core/exceptions.py,sha256=W29lbDlKE_yKdS-m_QHaWdDB1j3yGge6mVx5mo_jLrU,972
44
- fastapi_fastkit/core/settings.py,sha256=uIOpO0ZUdUcwpZAUAMHXpJfgYw70TNfZAwSG-lPjJDM,15701
44
+ fastapi_fastkit/core/settings.py,sha256=v9IiAyU4bIYf8haTCAupr_qiDFscAGlc4g_XqyZ0jrU,15675
45
45
  fastapi_fastkit/fastapi_project_template/PROJECT_README_TEMPLATE.md,sha256=5lT_ZY1QItTDbhKYNY6KlOoGlbSTBSrs9DJthcle3D4,1529
46
46
  fastapi_fastkit/fastapi_project_template/README.md,sha256=rXwDDK5rGNVd7xWuNkjuu5KWBE27Yz0v-EW1FNxldXA,6637
47
47
  fastapi_fastkit/fastapi_project_template/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -286,25 +286,25 @@ fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_chat.py-tp
286
286
  fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_health.py-tpl,sha256=JZwOjpKM_1R4vwTY41VbYgD0pNIxnXl_HUy7oWPqFAg,484
287
287
  fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_memory.py-tpl,sha256=MiQGVgEX0WdeMp7qFB31CFautIoYdHcOtHKm7eiO6e8,1272
288
288
  fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_tools.py-tpl,sha256=lDzyV70_YxpuymCOThFkNqXApYu4yd_tIk7lX6LJlLA,1439
289
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/.env-tpl,sha256=mmBW2Rz0aEIEuU6qDmr1jC5WF2vhU1u-A_7xsQe7iEc,664
290
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/README.md-tpl,sha256=V10bwMeygQPaNZZBkOxAZW0r7CDyDuiSXQK_FtMj8vA,6395
291
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/pyproject.toml-tpl,sha256=q6fg_Yua0MYlSZX8j3R8CyhYoVvN9Ex1d6wMZFo97WY,1750
292
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/requirements.txt-tpl,sha256=Q1_ZimB8ErrkwtzSWFudYrfsI8K2HcB8IphL74u5kNU,1208
289
+ fastapi_fastkit/fastapi_project_template/fastapi-mcp/.env-tpl,sha256=3styxb-X8yguh-V7-XjTWmd95fmnFf2AtLt2Q1kPn_w,695
290
+ fastapi_fastkit/fastapi_project_template/fastapi-mcp/README.md-tpl,sha256=zIyfUG1S80WDsdqQVqpR6sxH_QLi7GY3hHT6SdwEfHQ,6426
291
+ fastapi_fastkit/fastapi_project_template/fastapi-mcp/pyproject.toml-tpl,sha256=arYdZzGNJSjCouTAQKnmaeorVf-d4P98xqIItELedFA,1732
292
+ fastapi_fastkit/fastapi_project_template/fastapi-mcp/requirements.txt-tpl,sha256=yaOrA-JiT_UrNPyFCYxp3rL7ifVrZ__549Wh-3EmXvg,1172
293
293
  fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/format.sh-tpl,sha256=lMHoGl8naqGqYnW3WQ5zKJSkFFrWa4EvooA3xyj5Duk,215
294
294
  fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/lint.sh-tpl,sha256=TbnPdvBtO-k4XfAnYxUatrsXn4QniW6THmR0gwqshfs,397
295
295
  fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/run-server.sh-tpl,sha256=Uuti1QOukxvii8ss9RjTf8DzkyJ0V7HyJDGRPhjtazA,195
296
296
  fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/test.sh-tpl,sha256=FwcP6i9GGhAiUjyrvczr89z7zT4OEAGVOYQUOVM8vXU,185
297
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/setup.cfg-tpl,sha256=bJLOPvYv39uHfebufJHfXoc5tl9FOXIY37pOGZwWxO0,1940
297
+ fastapi_fastkit/fastapi_project_template/fastapi-mcp/setup.cfg-tpl,sha256=PsOhp-z9fEJFlHJI99AX3Gh6Ub0mZt52puvOf_r6ZLs,1932
298
298
  fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/__init__.py-tpl,sha256=bhmTXfHwW_cFBb18cxepy6TQxHYAI1swxEHxIlf15z4,78
299
299
  fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/__init__.py-tpl,sha256=BDaEDKpisCteGve4mGQWkAsrRPsPToytMgcdyF1IS30,46
300
300
  fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/api.py-tpl,sha256=MQknnIJ25KUX_nFgjWZR7bsNHsWWw9pLaus_0hL2b0Q,381
301
301
  fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/__init__.py-tpl,sha256=MgQT6uitMAu1OybStW9h3rZcq7OHreYo4oIvaI0skhM,27
302
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/auth.py-tpl,sha256=KgbkjuERg_5SWL39uAiSkrcby3aE8Ir6Ay_R-zdf8EY,4452
302
+ fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/auth.py-tpl,sha256=9uHoB275h3bcqr5O5aeV5pmoN57GhRrWk-dauc1ZnJU,4480
303
303
  fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/items.py-tpl,sha256=MSw3t3356SMy5sazaju_6dacPTiDInfUshe_EhjtXPw,5127
304
304
  fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/auth/__init__.py-tpl,sha256=G-psNiJfSbhWd8POcbLnjkPee8zgCCGFzNYTP1w94d0,53
305
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/auth/dependencies.py-tpl,sha256=WUA9UJoTnEObRSUJeEZm0qngFYhQsBmzTjsQG49rlnw,2977
305
+ fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/auth/dependencies.py-tpl,sha256=1sd-wEiOyhYF7xPGood1Muz2w-VswuEmQ5ajJm_bGHc,2963
306
306
  fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/core/__init__.py-tpl,sha256=mZUgAUouQG-XnGGliTe99fH7bQE9EKkVMAMyKXZdODE,37
307
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/core/config.py-tpl,sha256=aRlKfb4pbSfU0vgr4DNAgb1zBLcThuP_l0t_1jvlCvc,1587
307
+ fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/core/config.py-tpl,sha256=PKO2kx5--UZ0xfDh_oVBM6iNJhFu_1rUZkmWDmv4eWY,1608
308
308
  fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/main.py-tpl,sha256=f88-HGszl41NivIau0VsUoG8QmB69hDDADViVhvZv6A,1867
309
309
  fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/mcp_server/__init__.py-tpl,sha256=TC8tYJfqKT5hx6jucPcYUH2scDDSFCruCcU51_HOLEM,61
310
310
  fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/mcp_server/router.py-tpl,sha256=UDPdWPBRaPiQmvfvBfJBl73vcBuBXDQqOHKMB6mbaa8,2257
@@ -315,7 +315,7 @@ fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/conftest.py-tpl,sha25
315
315
  fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_auth.py-tpl,sha256=cmxKuioHgRF2YhbpRji0pl8_kF97WQML6v0rYYdOinc,3801
316
316
  fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_items.py-tpl,sha256=dIb_zyUVq_H-zQYqJvrhK2psaYdhLsKlOIiHTONuaeo,3280
317
317
  fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_mcp.py-tpl,sha256=noeUozEtFqNw3R3oSMMbmtBxDiUZhBwTWWZrI17qLmg,2641
318
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/.env-tpl,sha256=9aEuqkEeGGT_rJTV7HBmjCSqQDgr6BBaWOiAcWH9hyg,152
318
+ fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/.env-tpl,sha256=tCVg1X0u5jkrafMbnXulu8IDfbI9s7-hh9eZXveoAxw,261
319
319
  fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/.gitignore-tpl,sha256=8QwgyX8vpYhDtg1bcrBjf99yiLQRRO1OkBsGMRfggKY,269
320
320
  fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/Dockerfile-tpl,sha256=mwwhgtBVu94p4ke7Re0lg28G5vhKpeid1xttMvHwLls,325
321
321
  fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/README.md-tpl,sha256=959SSsG-ZCZ7edlWW817Sp6OlNkEARmQ0DogjsJ4iWo,3779
@@ -415,7 +415,7 @@ fastapi_fastkit/fastapi_project_template/modules/schemas/__init__.py-tpl,sha256=
415
415
  fastapi_fastkit/fastapi_project_template/modules/schemas/new_route.py-tpl,sha256=f83oXEj2wJ5lg620Bc0vRmpcjOTIpfMgejzzrHeerGk,569
416
416
  fastapi_fastkit/fragments/README.md,sha256=-Ch-2ZEbSkL6oXUkhaIkn3joR81E0ZLSJj1FaBPghW8,4962
417
417
  fastapi_fastkit/fragments/auth/fastapi_users.py.j2,sha256=ZFR7pdk4KLtSl9MxO5HzFRDMZU7QqN-6V7F6MI3P6GY,265
418
- fastapi_fastkit/fragments/auth/jwt.py.j2,sha256=0eZFVbsx_RS_gGSTMBwBeicQ_a3mZz3w_hZRE7Rxsw8,1299
418
+ fastapi_fastkit/fragments/auth/jwt.py.j2,sha256=DNWxo78LpvQQR7Iw_eu75uh-dn2ki5ZYJg5hiC7qXLY,1321
419
419
  fastapi_fastkit/fragments/auth/oauth2.py.j2,sha256=1k8bhiX8cmCmBSOaUwN7sIGNYbVLsEGYVj50D2EQmpE,3496
420
420
  fastapi_fastkit/fragments/auth/session.py.j2,sha256=ffaoUN4f6LvPTUTjWZXzbrOpCUJF1OmumiW1I6tlEOU,2176
421
421
  fastapi_fastkit/fragments/db/mongodb.py.j2,sha256=TKhvGbVK2eAO3qo-ogwNyR3529_a-mW5d7AXYe_CFr8,530
@@ -481,4 +481,4 @@ fastapi_fastkit/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
481
481
  fastapi_fastkit/utils/config_file.py,sha256=0rZEwCJblHabZlFibjYn2iycNZ8Rp4scOdZpfYtjU4g,9654
482
482
  fastapi_fastkit/utils/logging.py,sha256=oU7BnWInxzl_Gk8kGQ0f6cKdNHZcOVVcSHY4I9MTmR8,6431
483
483
  fastapi_fastkit/utils/main.py,sha256=dCuTSYCME7oCqlvicn-QFGEwA9MeNvMtfKcmThGewyU,13204
484
- fastapi_fastkit-1.4.0.dist-info/RECORD,,
484
+ fastapi_fastkit-1.4.1.dist-info/RECORD,,