forktex-cloud 2.1.0__tar.gz → 2.3.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 (37) hide show
  1. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/PKG-INFO +12 -11
  2. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/README.md +9 -7
  3. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/pyproject.toml +16 -5
  4. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/__init__.py +7 -5
  5. forktex_cloud-2.3.0/src/forktex_cloud/bridge/__init__.py +25 -0
  6. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/bridge/local_compose.py +32 -9
  7. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/bridge/loki.py +2 -2
  8. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/bridge/persistence_defaults.py +17 -4
  9. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/client/__init__.py +4 -3
  10. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/client/client.py +316 -625
  11. forktex_cloud-2.3.0/src/forktex_cloud/client/facade.py +393 -0
  12. forktex_cloud-2.3.0/src/forktex_cloud/client/generated/__init__.py +2643 -0
  13. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/config.py +6 -2
  14. forktex_cloud-2.3.0/src/forktex_cloud/local.py +178 -0
  15. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/manifest/loader.py +3 -0
  16. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/manifest/schema.py +11 -2
  17. forktex_cloud-2.3.0/src/forktex_cloud/manifest/semantics.py +130 -0
  18. forktex_cloud-2.3.0/src/forktex_cloud/scaffold/__init__.py +5 -0
  19. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/scaffold/templates.py +1 -1
  20. forktex_cloud-2.3.0/src/forktex_cloud/secrets/__init__.py +14 -0
  21. forktex_cloud-2.1.0/src/forktex_cloud/bridge/__init__.py +0 -1
  22. forktex_cloud-2.1.0/src/forktex_cloud/client/generated/__init__.py +0 -1907
  23. forktex_cloud-2.1.0/src/forktex_cloud/scaffold/__init__.py +0 -1
  24. forktex_cloud-2.1.0/src/forktex_cloud/secrets/__init__.py +0 -1
  25. forktex_cloud-2.1.0/src/forktex_cloud/vpn/__init__.py +0 -23
  26. forktex_cloud-2.1.0/src/forktex_cloud/vpn/local.py +0 -86
  27. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/LICENSE +0 -0
  28. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/bridge/log_formatter.py +0 -0
  29. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/manifest/__init__.py +0 -0
  30. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/manifest/errors.py +0 -0
  31. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/manifest/merge.py +0 -0
  32. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/secrets/base.py +0 -0
  33. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/secrets/factory.py +0 -0
  34. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/secrets/fernet.py +0 -0
  35. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/secrets/resolver.py +0 -0
  36. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/templates/observability/loki.yml +0 -0
  37. {forktex_cloud-2.1.0 → forktex_cloud-2.3.0}/src/forktex_cloud/templates/observability/promtail.yml +0 -0
@@ -1,21 +1,20 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forktex-cloud
3
- Version: 2.1.0
3
+ Version: 2.3.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
7
7
  Keywords: forktex,cloud,deployment,vps,hetzner,ansible,blue-green,iac,infrastructure-as-code,sdk
8
8
  Author: ForkTex
9
9
  Author-email: info@forktex.com
10
- Requires-Python: >=3.11
10
+ Requires-Python: >=3.14,<4.0
11
11
  Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Intended Audience :: System Administrators
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Classifier: Programming Language :: Python :: 3 :: Only
17
- Classifier: Programming Language :: Python :: 3.11
18
- Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.14
19
18
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
19
  Classifier: Topic :: System :: Distributed Computing
21
20
  Classifier: Topic :: System :: Installation/Setup
@@ -40,9 +39,7 @@ Description-Content-Type: text/markdown
40
39
 
41
40
  Standalone Python SDK for the [ForkTex Cloud](https://cloud.forktex.com) platform.
42
41
 
43
- `forktex-cloud` is the typed `httpx` client + manifest plumbing that the `forktex` CLI uses to talk to the ForkTex Cloud control plane: project provisioning, server management, deployments, vault, manifest validation, and the docker-compose / Hetzner / Ansible bridge.
44
-
45
- You can use it directly from any Python application — no `forktex` CLI required.
42
+ `forktex-cloud` is the typed `httpx` client + manifest plumbing for the ForkTex Cloud control plane: project provisioning, server management, deployments, vault, manifest validation, and the docker-compose / Hetzner / Ansible bridge. It was the library behind the retired `forktex cloud …` CLI; today it is used directly from Python applications, the platform's own runner, and CI.
46
43
 
47
44
  ## Install
48
45
 
@@ -50,7 +47,7 @@ You can use it directly from any Python application — no `forktex` CLI require
50
47
  pip install forktex-cloud
51
48
  ```
52
49
 
53
- Requires Python ≥ 3.11.
50
+ Requires Python ≥ 3.14 (`requires-python = ">=3.14,<4.0"`).
54
51
 
55
52
  ## Quick Start
56
53
 
@@ -93,6 +90,10 @@ client = Cloud(
93
90
  account_key="ftx-dev-key-2026",
94
91
  org_id="<your-org-uuid>",
95
92
  )
93
+
94
+ For integration tests or proxying through a mock/live transport, pass the
95
+ optional `transport=` argument to `Cloud(...)` and provide any `httpx`
96
+ transport implementation.
96
97
  ```
97
98
 
98
99
  ### Trigger a deploy pipeline
@@ -155,8 +156,8 @@ client.vault_delete("POSTGRES_PASSWORD")
155
156
  | `forktex_cloud.client` | Typed sync httpx client (`Cloud`) + all OpenAPI-codegenned Pydantic models (`ServerRead`, `ProjectRead`, `EventRead`, `VaultGetResponse`, ...) |
156
157
  | `forktex_cloud.manifest` | `Manifest` loader, discriminated-union schema (v1), deep-merge for env overlays, `ManifestError` |
157
158
  | `forktex_cloud.config` | `CloudContext` — controller URL, JWT / account-key, current org + project keys |
158
- | `forktex_cloud.scaffold` | `forktex cloud init` template generator (ProjectDeployment / StaticSite / SingleContainer / NativeBuild) |
159
- | `forktex_cloud.bridge` | docker-compose generator (local mode), Loki config, log formatters used by `forktex cloud apply --env local` |
159
+ | `forktex_cloud.scaffold` | `forktex.json` template generator (formerly driven by the retired `forktex cloud init` CLI) |
160
+ | `forktex_cloud.bridge` | docker-compose generator (local mode: `local_compose_from_manifest`, used by the repo's `scripts/local_stack.py` / `make start`; formerly by the retired `forktex cloud apply --env local` CLI), Loki config, log formatters |
160
161
  | `forktex_cloud.secrets` | Fernet vault + `${vault:KEY}` resolver for compile-time secret injection |
161
162
  | `forktex_cloud.paths` | Cross-platform `.forktex/` + `~/.forktex/` filesystem spec (V1). See [docs/forktex-directory-spec.md](https://github.com/forktex/cloud/blob/master/docs/forktex-directory-spec.md) |
162
163
 
@@ -191,7 +192,7 @@ The SDK follows [SemVer](https://semver.org/). The client's response models are
191
192
  This SDK lives inside the [`forktex/cloud`](https://github.com/forktex/cloud) monorepo alongside the API server (`api/`) and React Native client (`client/`). The SDK package is independently versioned and published to PyPI.
192
193
 
193
194
  - Docs: [https://github.com/forktex/cloud/tree/master/docs](https://github.com/forktex/cloud/tree/master/docs)
194
- - Production runbook: [production-runbook.md](https://github.com/forktex/cloud/blob/master/docs/production-runbook.md)
195
+ - Operations guide: [operations.md](https://github.com/forktex/cloud/blob/master/docs/operations.md)
195
196
  - Issues: [https://github.com/forktex/cloud/issues](https://github.com/forktex/cloud/issues)
196
197
 
197
198
  ## License
@@ -6,9 +6,7 @@
6
6
 
7
7
  Standalone Python SDK for the [ForkTex Cloud](https://cloud.forktex.com) platform.
8
8
 
9
- `forktex-cloud` is the typed `httpx` client + manifest plumbing that the `forktex` CLI uses to talk to the ForkTex Cloud control plane: project provisioning, server management, deployments, vault, manifest validation, and the docker-compose / Hetzner / Ansible bridge.
10
-
11
- You can use it directly from any Python application — no `forktex` CLI required.
9
+ `forktex-cloud` is the typed `httpx` client + manifest plumbing for the ForkTex Cloud control plane: project provisioning, server management, deployments, vault, manifest validation, and the docker-compose / Hetzner / Ansible bridge. It was the library behind the retired `forktex cloud …` CLI; today it is used directly from Python applications, the platform's own runner, and CI.
12
10
 
13
11
  ## Install
14
12
 
@@ -16,7 +14,7 @@ You can use it directly from any Python application — no `forktex` CLI require
16
14
  pip install forktex-cloud
17
15
  ```
18
16
 
19
- Requires Python ≥ 3.11.
17
+ Requires Python ≥ 3.14 (`requires-python = ">=3.14,<4.0"`).
20
18
 
21
19
  ## Quick Start
22
20
 
@@ -59,6 +57,10 @@ client = Cloud(
59
57
  account_key="ftx-dev-key-2026",
60
58
  org_id="<your-org-uuid>",
61
59
  )
60
+
61
+ For integration tests or proxying through a mock/live transport, pass the
62
+ optional `transport=` argument to `Cloud(...)` and provide any `httpx`
63
+ transport implementation.
62
64
  ```
63
65
 
64
66
  ### Trigger a deploy pipeline
@@ -121,8 +123,8 @@ client.vault_delete("POSTGRES_PASSWORD")
121
123
  | `forktex_cloud.client` | Typed sync httpx client (`Cloud`) + all OpenAPI-codegenned Pydantic models (`ServerRead`, `ProjectRead`, `EventRead`, `VaultGetResponse`, ...) |
122
124
  | `forktex_cloud.manifest` | `Manifest` loader, discriminated-union schema (v1), deep-merge for env overlays, `ManifestError` |
123
125
  | `forktex_cloud.config` | `CloudContext` — controller URL, JWT / account-key, current org + project keys |
124
- | `forktex_cloud.scaffold` | `forktex cloud init` template generator (ProjectDeployment / StaticSite / SingleContainer / NativeBuild) |
125
- | `forktex_cloud.bridge` | docker-compose generator (local mode), Loki config, log formatters used by `forktex cloud apply --env local` |
126
+ | `forktex_cloud.scaffold` | `forktex.json` template generator (formerly driven by the retired `forktex cloud init` CLI) |
127
+ | `forktex_cloud.bridge` | docker-compose generator (local mode: `local_compose_from_manifest`, used by the repo's `scripts/local_stack.py` / `make start`; formerly by the retired `forktex cloud apply --env local` CLI), Loki config, log formatters |
126
128
  | `forktex_cloud.secrets` | Fernet vault + `${vault:KEY}` resolver for compile-time secret injection |
127
129
  | `forktex_cloud.paths` | Cross-platform `.forktex/` + `~/.forktex/` filesystem spec (V1). See [docs/forktex-directory-spec.md](https://github.com/forktex/cloud/blob/master/docs/forktex-directory-spec.md) |
128
130
 
@@ -157,7 +159,7 @@ The SDK follows [SemVer](https://semver.org/). The client's response models are
157
159
  This SDK lives inside the [`forktex/cloud`](https://github.com/forktex/cloud) monorepo alongside the API server (`api/`) and React Native client (`client/`). The SDK package is independently versioned and published to PyPI.
158
160
 
159
161
  - Docs: [https://github.com/forktex/cloud/tree/master/docs](https://github.com/forktex/cloud/tree/master/docs)
160
- - Production runbook: [production-runbook.md](https://github.com/forktex/cloud/blob/master/docs/production-runbook.md)
162
+ - Operations guide: [operations.md](https://github.com/forktex/cloud/blob/master/docs/operations.md)
161
163
  - Issues: [https://github.com/forktex/cloud/issues](https://github.com/forktex/cloud/issues)
162
164
 
163
165
  ## License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forktex-cloud"
3
- version = "2.1.0"
3
+ version = "2.3.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"}
@@ -8,7 +8,7 @@ authors = [
8
8
  readme = "README.md"
9
9
  license = "MIT"
10
10
  license-files = ["LICENSE"]
11
- requires-python = ">=3.11"
11
+ requires-python = ">=3.14,<4.0"
12
12
  keywords = [
13
13
  "forktex",
14
14
  "cloud",
@@ -28,8 +28,7 @@ classifiers = [
28
28
  "Operating System :: OS Independent",
29
29
  "Programming Language :: Python :: 3",
30
30
  "Programming Language :: Python :: 3 :: Only",
31
- "Programming Language :: Python :: 3.11",
32
- "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.14",
33
32
  "Topic :: Software Development :: Libraries :: Python Modules",
34
33
  "Topic :: System :: Distributed Computing",
35
34
  "Topic :: System :: Installation/Setup",
@@ -61,10 +60,15 @@ dev = [
61
60
  "ruff (>=0.8.0)",
62
61
  "pyright (>=1.1.0,<2.0.0)",
63
62
  "mypy (>=1.11.0,<2.0.0)",
63
+ # Release tooling. `make build` and `make publish` shell out to these, so
64
+ # undeclared they turn a release into "No module named twine" at the moment
65
+ # you try to ship. Same pair, same versions, as ../forktex-py.
66
+ "build (>=1.2.0)",
67
+ "twine (>=5.0.0)",
64
68
  ]
65
69
 
66
70
  [tool.ruff]
67
- target-version = "py311"
71
+ target-version = "py314"
68
72
  line-length = 100
69
73
  extend-exclude = [
70
74
  # Auto-generated by OpenAPI codegen. Canonical import shape comes from
@@ -76,6 +80,13 @@ extend-exclude = [
76
80
  [tool.ruff.lint]
77
81
  select = ["E", "F", "I", "W"]
78
82
 
83
+ [tool.mypy]
84
+ plugins = ["pydantic.mypy"]
85
+ ignore_missing_imports = true
86
+ # Auto-generated from the OpenAPI spec — committed for drift-checking but not
87
+ # subject to hand-typing (mirrors the api codegen/build exclude).
88
+ exclude = ["client/generated"]
89
+
79
90
  [tool.pytest.ini_options]
80
91
  asyncio_mode = "auto"
81
92
  testpaths = ["tests"]
@@ -41,8 +41,10 @@ __version__ = "2.1.0"
41
41
  # model lookups hit the module-level cache populated by __getattr__.
42
42
  _LAZY_ATTRS: dict[str, tuple[str, str]] = {
43
43
  # Client (httpx-backed, heavy)
44
- "Cloud": ("forktex_cloud.client.client", "Cloud"),
44
+ "Cloud": ("forktex_cloud.client.facade", "Cloud"),
45
+ "CloudClient": ("forktex_cloud.client.client", "CloudClient"),
45
46
  "CloudAPIError": ("forktex_cloud.client.client", "CloudAPIError"),
47
+ "DeployBlocked": ("forktex_cloud.client.client", "DeployBlocked"),
46
48
  # Config
47
49
  "CloudContext": ("forktex_cloud.config", "CloudContext"),
48
50
  # Manifest
@@ -65,7 +67,6 @@ _LAZY_ATTRS: dict[str, tuple[str, str]] = {
65
67
  "TokenResponse": ("forktex_cloud.client.generated", "TokenResponse"),
66
68
  "UserRead": ("forktex_cloud.client.generated", "UserRead"),
67
69
  "VaultGetResponse": ("forktex_cloud.client.generated", "VaultGetResponse"),
68
- "WorkspaceRead": ("forktex_cloud.client.generated", "WorkspaceRead"),
69
70
  }
70
71
 
71
72
 
@@ -93,7 +94,8 @@ def __dir__() -> list[str]:
93
94
  # Static type-checker hint — declared as a TYPE_CHECKING block so it costs
94
95
  # nothing at runtime but lets editors / mypy see the eventual symbol types.
95
96
  if TYPE_CHECKING: # pragma: no cover
96
- from forktex_cloud.client.client import Cloud, CloudAPIError
97
+ from forktex_cloud.client.client import CloudAPIError, CloudClient, DeployBlocked
98
+ from forktex_cloud.client.facade import Cloud
97
99
  from forktex_cloud.client.generated import (
98
100
  SPEC_HASH,
99
101
  SPEC_VERSION,
@@ -111,7 +113,6 @@ if TYPE_CHECKING: # pragma: no cover
111
113
  TokenResponse,
112
114
  UserRead,
113
115
  VaultGetResponse,
114
- WorkspaceRead,
115
116
  )
116
117
  from forktex_cloud.config import CloudContext
117
118
  from forktex_cloud.manifest.loader import Manifest, ManifestError
@@ -123,7 +124,9 @@ __all__ = [
123
124
  "SPEC_HASH",
124
125
  # Client — lazy
125
126
  "Cloud",
127
+ "CloudClient",
126
128
  "CloudAPIError",
129
+ "DeployBlocked",
127
130
  # Config — lazy
128
131
  "CloudContext",
129
132
  # Manifest — lazy
@@ -144,5 +147,4 @@ __all__ = [
144
147
  "TokenResponse",
145
148
  "UserRead",
146
149
  "VaultGetResponse",
147
- "WorkspaceRead",
148
150
  ]
@@ -0,0 +1,25 @@
1
+ """Bridge modules: manifest → docker compose for local dev, plus log helpers.
2
+
3
+ ``local_compose_from_manifest`` is the pure renderer the cloud repo's
4
+ ``scripts/local_stack.py`` drives; it returns a compose dict and writes nothing.
5
+ """
6
+
7
+ from forktex_cloud.bridge.local_compose import (
8
+ local_compose_from_manifest,
9
+ render_observability_configs,
10
+ )
11
+ from forktex_cloud.bridge.log_formatter import assign_colors, format_line
12
+ from forktex_cloud.bridge.loki import build_logql, loki_ready, query_range, tail
13
+ from forktex_cloud.bridge.persistence_defaults import detect_persistence_defaults
14
+
15
+ __all__ = [
16
+ "assign_colors",
17
+ "build_logql",
18
+ "detect_persistence_defaults",
19
+ "format_line",
20
+ "local_compose_from_manifest",
21
+ "loki_ready",
22
+ "query_range",
23
+ "render_observability_configs",
24
+ "tail",
25
+ ]
@@ -194,6 +194,22 @@ def _add_observability_services(
194
194
  }
195
195
 
196
196
 
197
+ def _observability_enabled(manifest: Manifest) -> bool:
198
+ """Whether the manifest asks for the local Loki/Promtail pair.
199
+
200
+ The `observability` argument is the caller's veto, not the answer: a
201
+ manifest that sets `observability.enabled: false` — as a project whose
202
+ local stack runs beside another one does, to keep Loki's 3100 free — still
203
+ got both containers, because the flag defaulted to True and nothing
204
+ consulted the manifest.
205
+ """
206
+ cloud = getattr(manifest, "cloud", None)
207
+ obs = getattr(cloud, "observability", None)
208
+ if obs is None:
209
+ return True
210
+ return bool(getattr(obs, "enabled", True))
211
+
212
+
197
213
  def local_compose_from_manifest(
198
214
  manifest: Manifest,
199
215
  project_root: Path,
@@ -245,8 +261,8 @@ def local_compose_from_manifest(
245
261
  # behind the initializer's start. We can't wait on `service_completed`
246
262
  # for a true oneshot the way k8s init-containers do, but the
247
263
  # ``service_started`` ordering closes the most common race (e.g.
248
- # gitea-init writes the token file → api can read it on its first
249
- # gitea call). The initializer itself self-polls its dependencies.
264
+ # an initializer writes state the primary reads on its first call).
265
+ # The initializer itself self-polls its dependencies.
250
266
  oneshot_ids: list[str] = [
251
267
  svc_def["id"]
252
268
  for svc_def in local_services
@@ -280,7 +296,7 @@ def local_compose_from_manifest(
280
296
  build_entry["dockerfile"] = dockerfile
281
297
  _require_dockerfile(ctx_dir, dockerfile, sid=sid, context=ctx)
282
298
  svc["build"] = build_entry
283
- elif svc_type == "compute":
299
+ elif svc_type in ("compute", "daemon"):
284
300
  found = _autodetect_dockerfile(project_root / sid, env_name)
285
301
  if found is not None:
286
302
  build_entry = {"context": _project_relative(f"./{sid}", root_prefix)}
@@ -291,14 +307,18 @@ def local_compose_from_manifest(
291
307
  if sid in host_ports:
292
308
  host_port = host_ports[sid]
293
309
  svc["ports"] = [f"{host_port}:{port}"]
310
+ # Additional host:container mappings (e.g. mailhog UI 8025:8025).
311
+ extra_ports = svc_def.get("extraPorts") or []
312
+ if extra_ports:
313
+ svc.setdefault("ports", []).extend(str(p) for p in extra_ports)
294
314
 
295
- # One-shot init services (e.g. `gitea-init`) carry the
315
+ # One-shot init services carry the
296
316
  # `forktex.oneshot=true` label so the generator skips the implicit
297
317
  # restart-always; they should exit cleanly after their seed step
298
318
  # and stay exited. The label lives on the existing ServiceDef
299
319
  # `labels` field — no schema/codegen change required.
300
320
  is_oneshot = (svc_def.get("labels") or {}).get("forktex.oneshot") == "true"
301
- if svc_type in ("persistence", "observability") and not is_oneshot:
321
+ if svc_type in ("persistence", "daemon", "observability") and not is_oneshot:
302
322
  svc["restart"] = "always"
303
323
  if svc_def.get("labels"):
304
324
  svc["labels"] = dict(svc_def["labels"])
@@ -349,7 +369,7 @@ def local_compose_from_manifest(
349
369
  if svc_type == "persistence":
350
370
  # Bind-mount persistence data under .forktex/data/{sid}/ so it
351
371
  # survives `docker compose down -v` and is visible on the host
352
- # for inspection + backups. Mirrors api/src/bridge/local_compose.py.
372
+ # for inspection + backups.
353
373
  defaults = detect_persistence_defaults(image)
354
374
  if defaults and defaults.get("default_volume"):
355
375
  target = defaults["default_volume"]
@@ -362,7 +382,7 @@ def local_compose_from_manifest(
362
382
  if cmd:
363
383
  svc["command"] = cmd
364
384
 
365
- if svc_type == "compute":
385
+ if svc_type in ("compute", "daemon"):
366
386
  depends: dict[str, Any] = {}
367
387
  for pid in persistence_ids:
368
388
  healthy = persistence_has_healthcheck.get(pid)
@@ -371,7 +391,10 @@ def local_compose_from_manifest(
371
391
  for iid in oneshot_ids:
372
392
  # Initializers run side-by-side; we only need start-ordering,
373
393
  # not completion. The initializer's contract is "write its
374
- # output before the first compute request needs it."
394
+ # output before the first compute request needs it." A oneshot
395
+ # that is itself a daemon must not wait on itself.
396
+ if iid == sid:
397
+ continue
375
398
  depends[iid] = {"condition": "service_started"}
376
399
  if depends:
377
400
  svc["depends_on"] = depends
@@ -383,7 +406,7 @@ def local_compose_from_manifest(
383
406
  svc["networks"] = ["forktex"]
384
407
  services[sid] = svc
385
408
 
386
- if observability:
409
+ if observability and _observability_enabled(manifest):
387
410
  _add_observability_services(services, named_volumes)
388
411
 
389
412
  compose: dict[str, Any] = {"services": services}
@@ -16,7 +16,7 @@ def loki_ready(base_url: str = "http://localhost:3100") -> bool:
16
16
  req = urllib.request.Request(f"{base_url}/ready", method="GET")
17
17
  with urllib.request.urlopen(req, timeout=2) as resp:
18
18
  return resp.status == 200
19
- except (urllib.error.URLError, OSError):
19
+ except urllib.error.URLError, OSError:
20
20
  return False
21
21
 
22
22
 
@@ -77,7 +77,7 @@ def tail(
77
77
  now_ns = int(time.time() * 1_000_000_000)
78
78
  try:
79
79
  entries = query_range(base_url, logql, last_ts + 1, now_ns)
80
- except (urllib.error.URLError, OSError):
80
+ except urllib.error.URLError, OSError:
81
81
  return
82
82
  for ts_ns, service, line in entries:
83
83
  yield (ts_ns, service, line)
@@ -60,11 +60,24 @@ PERSISTENCE_DEFAULTS: dict[str, dict[str, Any]] = {
60
60
  "default_volume": "/data/db",
61
61
  },
62
62
  "qdrant": {
63
+ # The qdrant image ships no wget, curl or nc — only bash. So the probe
64
+ # speaks HTTP over /dev/tcp rather than shelling out to a client that is
65
+ # not there. It must be `CMD` + an explicit `bash -c`: /dev/tcp is a bash
66
+ # builtin, and `CMD-SHELL` runs /bin/sh, where it fails with "cannot
67
+ # create /dev/tcp/..." — indistinguishable from a down node.
68
+ # `/readyz` rather than a bare port open: a bound socket is not a node
69
+ # that is ready to serve.
63
70
  "healthcheck": {
64
- "test": ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/6333'"],
65
- "interval": "5s",
66
- "timeout": "5s",
67
- "retries": 10,
71
+ "test": [
72
+ "CMD",
73
+ "bash",
74
+ "-c",
75
+ 'exec 3<>/dev/tcp/localhost/6333; printf "GET /readyz HTTP/1.0\\r\\n\\r\\n" >&3; '
76
+ 'head -1 <&3 | grep -q "200 OK"',
77
+ ],
78
+ "interval": "30s",
79
+ "timeout": "10s",
80
+ "retries": 5,
68
81
  },
69
82
  "default_port": 6333,
70
83
  "default_volume": "/qdrant/storage",
@@ -6,7 +6,8 @@ commonly used ones so call sites can ``from forktex_cloud.client import X``
6
6
  without touching the ``generated`` namespace directly.
7
7
  """
8
8
 
9
- from forktex_cloud.client.client import Cloud, CloudAPIError
9
+ from forktex_cloud.client.client import CloudAPIError, CloudClient, DeployBlocked
10
+ from forktex_cloud.client.facade import Cloud
10
11
  from forktex_cloud.client.generated import (
11
12
  ApiKeyCreated,
12
13
  ApiKeyRead,
@@ -31,12 +32,13 @@ from forktex_cloud.client.generated import (
31
32
  TokenResponse,
32
33
  UserRead,
33
34
  VaultGetResponse,
34
- WorkspaceRead,
35
35
  )
36
36
 
37
37
  __all__ = [
38
38
  "Cloud",
39
+ "CloudClient",
39
40
  "CloudAPIError",
41
+ "DeployBlocked",
40
42
  "ApiKeyCreated",
41
43
  "ApiKeyRead",
42
44
  "CapacityOrgLimits",
@@ -60,5 +62,4 @@ __all__ = [
60
62
  "TokenResponse",
61
63
  "UserRead",
62
64
  "VaultGetResponse",
63
- "WorkspaceRead",
64
65
  ]