forktex-cloud 2.0.0__tar.gz → 2.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.
Files changed (30) hide show
  1. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/PKG-INFO +1 -1
  2. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/pyproject.toml +1 -1
  3. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/__init__.py +1 -1
  4. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/bridge/local_compose.py +95 -11
  5. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/LICENSE +0 -0
  6. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/README.md +0 -0
  7. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/bridge/__init__.py +0 -0
  8. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/bridge/log_formatter.py +0 -0
  9. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/bridge/loki.py +0 -0
  10. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/bridge/persistence_defaults.py +0 -0
  11. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/client/__init__.py +0 -0
  12. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/client/client.py +0 -0
  13. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/client/generated/__init__.py +0 -0
  14. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/config.py +0 -0
  15. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/manifest/__init__.py +0 -0
  16. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/manifest/errors.py +0 -0
  17. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/manifest/loader.py +0 -0
  18. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/manifest/merge.py +0 -0
  19. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/manifest/schema.py +0 -0
  20. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/scaffold/__init__.py +0 -0
  21. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/scaffold/templates.py +0 -0
  22. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/secrets/__init__.py +0 -0
  23. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/secrets/base.py +0 -0
  24. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/secrets/factory.py +0 -0
  25. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/secrets/fernet.py +0 -0
  26. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/secrets/resolver.py +0 -0
  27. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/templates/observability/loki.yml +0 -0
  28. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/templates/observability/promtail.yml +0 -0
  29. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/vpn/__init__.py +0 -0
  30. {forktex_cloud-2.0.0 → forktex_cloud-2.1.0}/src/forktex_cloud/vpn/local.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forktex-cloud
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: Typed Python SDK for the ForkTex Cloud platform — provision, deploy, and manage VPS-backed apps via a declarative manifest.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forktex-cloud"
3
- version = "2.0.0"
3
+ version = "2.1.0"
4
4
  description = "Typed Python SDK for the ForkTex Cloud platform — provision, deploy, and manage VPS-backed apps via a declarative manifest."
5
5
  authors = [
6
6
  {name = "ForkTex", email = "info@forktex.com"}
@@ -32,7 +32,7 @@ from __future__ import annotations
32
32
 
33
33
  from typing import TYPE_CHECKING, Any
34
34
 
35
- __version__ = "2.0.0"
35
+ __version__ = "2.1.0"
36
36
 
37
37
  # ── Lazy attribute map ──────────────────────────────────────────────────────
38
38
  #
@@ -2,6 +2,13 @@
2
2
 
3
3
  Generates a simple local-oriented docker-compose.local.yml from a forktex manifest.
4
4
  No proxy, no blue-green, no SSL -- just plain containers with direct port mapping.
5
+
6
+ Build-context convention: a service's ``build.context`` is interpreted relative
7
+ to the PROJECT ROOT (a leading ``./`` is optional; ``.`` means the root). The
8
+ generator re-bases it onto the generated compose file's directory via
9
+ ``root_prefix``. A context may not escape the project root (``..``) — each
10
+ service builds from inside its own project. ``build.dockerfile`` is relative to
11
+ the resolved context and must exist (both are validated at generation time).
5
12
  """
6
13
 
7
14
  from __future__ import annotations
@@ -10,6 +17,7 @@ from pathlib import Path
10
17
  from typing import Any
11
18
 
12
19
  from forktex_cloud.bridge.persistence_defaults import detect_persistence_defaults
20
+ from forktex_cloud.manifest.errors import ManifestError
13
21
  from forktex_cloud.manifest.loader import Manifest
14
22
  from forktex_cloud.secrets.base import SecretsProvider
15
23
 
@@ -17,6 +25,76 @@ from forktex_cloud.secrets.base import SecretsProvider
17
25
  _OBSERVABILITY_PORTS = {3100}
18
26
 
19
27
 
28
+ def _project_relative(path: str, root_prefix: str) -> str:
29
+ """Express a project-root-relative ``path`` relative to the compose dir.
30
+
31
+ The compose file is generated below the project root (``root_prefix`` is the
32
+ hop back up — e.g. ``../..`` for ``.forktex/cache/``), so a context or bind
33
+ source the manifest states *relative to the project root* must be re-based
34
+ onto it. A leading ``./`` is optional; ``.`` / ``""`` mean the root itself.
35
+ """
36
+ stripped = path.removeprefix("./")
37
+ if stripped in (".", ""):
38
+ return root_prefix
39
+ return f"{root_prefix}/{stripped}".rstrip("/")
40
+
41
+
42
+ def _resolve_build_context(
43
+ ctx: str, *, sid: str, project_root: Path, root_prefix: str
44
+ ) -> tuple[str, Path]:
45
+ """Resolve a manifest build ``context`` to ``(compose-relative, on-disk dir)``.
46
+
47
+ Relative contexts are project-root-relative (leading ``./`` optional);
48
+ absolute contexts pass through untouched. A relative context that escapes
49
+ the project root (via ``..``) is rejected — every service builds from inside
50
+ its own project (see ``standard.forktex-architecture``); a context reaching
51
+ the surrounding workspace is the smell that produced the ``network/network``
52
+ double-path bug.
53
+ """
54
+ if ctx.startswith("/"):
55
+ return ctx, Path(ctx)
56
+ stripped = ctx.removeprefix("./")
57
+ on_disk = (project_root / stripped).resolve()
58
+ root = project_root.resolve()
59
+ if on_disk != root and root not in on_disk.parents:
60
+ raise ManifestError(
61
+ f"service {sid!r}: build context {ctx!r} escapes the project root "
62
+ f"(resolves to {on_disk}, outside {root}). Build contexts must stay "
63
+ f"inside the project — build the service from its own directory."
64
+ )
65
+ return _project_relative(ctx, root_prefix), on_disk
66
+
67
+
68
+ def _require_dockerfile(ctx_dir: Path, dockerfile: str | None, *, sid: str, context: str) -> None:
69
+ """Fail with a clear error if the Dockerfile is missing under the context.
70
+
71
+ The ``dockerfile`` path is interpreted relative to ``context``; when the two
72
+ desync Docker emits only a cryptic ``lstat`` failure at build time. Surface
73
+ it at generation time instead.
74
+ """
75
+ rel = dockerfile or "Dockerfile"
76
+ target = ctx_dir / rel
77
+ if not target.is_file():
78
+ raise ManifestError(
79
+ f"service {sid!r}: dockerfile {rel!r} not found under build context "
80
+ f"{context!r} (looked for {target}). `dockerfile` is relative to "
81
+ f"`context`."
82
+ )
83
+
84
+
85
+ def _autodetect_dockerfile(svc_dir: Path, env_name: str) -> Path | None:
86
+ """Find a buildable Dockerfile in ``svc_dir`` — env-specific first.
87
+
88
+ A local build wants ``Dockerfile.local`` over the prod ``Dockerfile`` when
89
+ both exist; fall back to the plain name otherwise.
90
+ """
91
+ for name in (f"Dockerfile.{env_name}", "Dockerfile"):
92
+ candidate = svc_dir / name
93
+ if candidate.is_file():
94
+ return candidate
95
+ return None
96
+
97
+
20
98
  def _allocate_host_ports(
21
99
  services: list[dict[str, Any]],
22
100
  *,
@@ -186,23 +264,29 @@ def local_compose_from_manifest(
186
264
  # Build context — explicit overlay first, else auto-detect a sibling
187
265
  # Dockerfile for compute services. Persistence services only opt in
188
266
  # when the manifest explicitly declares `build` (zot is the canonical
189
- # case — persistence-typed but first-party).
267
+ # case — persistence-typed but first-party). `context` is interpreted
268
+ # relative to the PROJECT ROOT (leading `./` optional) and re-based onto
269
+ # the compose dir via root_prefix; a `dockerfile` is relative to it.
190
270
  build_cfg = svc_def.get("build")
191
271
  if build_cfg and isinstance(build_cfg, dict):
192
272
  build_entry: dict[str, str] = {}
193
273
  ctx = build_cfg.get("context", f"./{sid}")
194
- # Rewrite a project-relative context to be relative to the compose
195
- # file's directory (.forktex/cache/) via root_prefix.
196
- build_entry["context"] = (
197
- f"{root_prefix}/{ctx.removeprefix('./')}" if ctx.startswith("./") else ctx
274
+ compose_ctx, ctx_dir = _resolve_build_context(
275
+ ctx, sid=sid, project_root=project_root, root_prefix=root_prefix
198
276
  )
199
- if build_cfg.get("dockerfile"):
200
- build_entry["dockerfile"] = build_cfg["dockerfile"]
277
+ build_entry["context"] = compose_ctx
278
+ dockerfile = build_cfg.get("dockerfile")
279
+ if dockerfile:
280
+ build_entry["dockerfile"] = dockerfile
281
+ _require_dockerfile(ctx_dir, dockerfile, sid=sid, context=ctx)
201
282
  svc["build"] = build_entry
202
283
  elif svc_type == "compute":
203
- dockerfile = project_root / sid / "Dockerfile"
204
- if dockerfile.is_file():
205
- svc["build"] = {"context": f"{root_prefix}/{sid}"}
284
+ found = _autodetect_dockerfile(project_root / sid, env_name)
285
+ if found is not None:
286
+ build_entry = {"context": _project_relative(f"./{sid}", root_prefix)}
287
+ if found.name != "Dockerfile":
288
+ build_entry["dockerfile"] = found.name
289
+ svc["build"] = build_entry
206
290
 
207
291
  if sid in host_ports:
208
292
  host_port = host_ports[sid]
@@ -252,7 +336,7 @@ def local_compose_from_manifest(
252
336
  src = v.split(":")[0]
253
337
  rest = v[len(src) :]
254
338
  if src.startswith("./"):
255
- rewritten.append(f"{root_prefix}/{src[2:]}{rest}")
339
+ rewritten.append(f"{_project_relative(src, root_prefix)}{rest}")
256
340
  elif src.startswith("/") or not src.startswith("."):
257
341
  rewritten.append(v)
258
342
  if not src.startswith("/"):
File without changes
File without changes