blobhub-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.
- blobhub_cli-0.1.0/.gitignore +28 -0
- blobhub_cli-0.1.0/LICENSE +21 -0
- blobhub_cli-0.1.0/PKG-INFO +189 -0
- blobhub_cli-0.1.0/README.md +136 -0
- blobhub_cli-0.1.0/pyproject.toml +58 -0
- blobhub_cli-0.1.0/src/blobhub_cli/__init__.py +8 -0
- blobhub_cli-0.1.0/src/blobhub_cli/__main__.py +11 -0
- blobhub_cli-0.1.0/src/blobhub_cli/api/__init__.py +8 -0
- blobhub_cli-0.1.0/src/blobhub_cli/api/client.py +126 -0
- blobhub_cli-0.1.0/src/blobhub_cli/api/commands.py +157 -0
- blobhub_cli-0.1.0/src/blobhub_cli/api/errors.py +51 -0
- blobhub_cli-0.1.0/src/blobhub_cli/blob/__init__.py +9 -0
- blobhub_cli-0.1.0/src/blobhub_cli/blob/reference.py +87 -0
- blobhub_cli-0.1.0/src/blobhub_cli/cli/__init__.py +5 -0
- blobhub_cli-0.1.0/src/blobhub_cli/cli/app.py +364 -0
- blobhub_cli-0.1.0/src/blobhub_cli/codes.py +99 -0
- blobhub_cli-0.1.0/src/blobhub_cli/commands/__init__.py +7 -0
- blobhub_cli-0.1.0/src/blobhub_cli/commands/auth.py +147 -0
- blobhub_cli-0.1.0/src/blobhub_cli/commands/blob.py +187 -0
- blobhub_cli-0.1.0/src/blobhub_cli/commands/completion.py +35 -0
- blobhub_cli-0.1.0/src/blobhub_cli/commands/doctor.py +245 -0
- blobhub_cli-0.1.0/src/blobhub_cli/commands/eject.py +153 -0
- blobhub_cli-0.1.0/src/blobhub_cli/commands/execute.py +124 -0
- blobhub_cli-0.1.0/src/blobhub_cli/commands/scheduler.py +556 -0
- blobhub_cli-0.1.0/src/blobhub_cli/commands/workflow.py +798 -0
- blobhub_cli-0.1.0/src/blobhub_cli/compiler/__init__.py +8 -0
- blobhub_cli-0.1.0/src/blobhub_cli/compiler/allowlist.py +141 -0
- blobhub_cli-0.1.0/src/blobhub_cli/compiler/emit.py +109 -0
- blobhub_cli-0.1.0/src/blobhub_cli/compiler/graph.py +360 -0
- blobhub_cli-0.1.0/src/blobhub_cli/compiler/sandbox.py +56 -0
- blobhub_cli-0.1.0/src/blobhub_cli/config/__init__.py +7 -0
- blobhub_cli-0.1.0/src/blobhub_cli/config/credentials.py +57 -0
- blobhub_cli-0.1.0/src/blobhub_cli/config/settings.py +69 -0
- blobhub_cli-0.1.0/src/blobhub_cli/console.py +93 -0
- blobhub_cli-0.1.0/src/blobhub_cli/definition/__init__.py +8 -0
- blobhub_cli-0.1.0/src/blobhub_cli/definition/drift.py +40 -0
- blobhub_cli-0.1.0/src/blobhub_cli/definition/eject.py +64 -0
- blobhub_cli-0.1.0/src/blobhub_cli/definition/hashing.py +45 -0
- blobhub_cli-0.1.0/src/blobhub_cli/definition/model.py +105 -0
- blobhub_cli-0.1.0/src/blobhub_cli/exit.py +16 -0
- blobhub_cli-0.1.0/src/blobhub_cli/formats/__init__.py +7 -0
- blobhub_cli-0.1.0/src/blobhub_cli/formats/io.py +86 -0
- blobhub_cli-0.1.0/src/blobhub_cli/ids.py +18 -0
- blobhub_cli-0.1.0/src/blobhub_cli/limits.py +34 -0
- blobhub_cli-0.1.0/src/blobhub_cli/manifest/__init__.py +8 -0
- blobhub_cli-0.1.0/src/blobhub_cli/manifest/base.py +93 -0
- blobhub_cli-0.1.0/src/blobhub_cli/manifest/resolver.py +120 -0
- blobhub_cli-0.1.0/src/blobhub_cli/manifest/scheduler.py +237 -0
- blobhub_cli-0.1.0/src/blobhub_cli/manifest/workflow.py +141 -0
- blobhub_cli-0.1.0/src/blobhub_cli/scheduler/__init__.py +10 -0
- blobhub_cli-0.1.0/src/blobhub_cli/scheduler/references.py +260 -0
- blobhub_cli-0.1.0/src/blobhub_cli/scheduler/schedules.py +93 -0
- blobhub_cli-0.1.0/src/blobhub_cli/statestore.py +50 -0
- blobhub_cli-0.1.0/src/blobhub_cli/workflow/__init__.py +8 -0
- blobhub_cli-0.1.0/src/blobhub_cli/workflow/definitions.py +141 -0
- blobhub_cli-0.1.0/src/blobhub_cli/workflow/executions.py +107 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Worktrees (workspace convention: one worktree per feature, under .worktrees/)
|
|
2
|
+
.worktrees/
|
|
3
|
+
|
|
4
|
+
# Temporary files (workspace convention: never store them outside the repo)
|
|
5
|
+
.tmp/
|
|
6
|
+
|
|
7
|
+
# Agent scratch (subagent-driven-development ledger, briefs, review packages)
|
|
8
|
+
.superpowers/
|
|
9
|
+
|
|
10
|
+
# Python
|
|
11
|
+
__pycache__/
|
|
12
|
+
*.py[cod]
|
|
13
|
+
*.egg-info/
|
|
14
|
+
build/
|
|
15
|
+
dist/
|
|
16
|
+
.venv/
|
|
17
|
+
venv/
|
|
18
|
+
|
|
19
|
+
# Tooling caches
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.ruff_cache/
|
|
22
|
+
.coverage
|
|
23
|
+
htmlcov/
|
|
24
|
+
|
|
25
|
+
# Editors / OS
|
|
26
|
+
.DS_Store
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BlobHub. All rights reserved.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: blobhub-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: BlobHub command line interface — work with blobs, revisions, and their contents from the terminal
|
|
5
|
+
Project-URL: Homepage, https://blobhub.io/
|
|
6
|
+
Project-URL: Documentation, https://docs.blobhub.io/
|
|
7
|
+
Project-URL: Source, https://github.com/blobhubio/blobhub-cli
|
|
8
|
+
Project-URL: Issues, https://github.com/blobhubio/blobhub-cli/issues
|
|
9
|
+
Author-email: BlobHub <developers@blobhub.io>
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 BlobHub. All rights reserved.
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: automation,blobhub,blobs,cli,command-line,versioning,workflows
|
|
33
|
+
Classifier: Development Status :: 4 - Beta
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: MacOS
|
|
37
|
+
Classifier: Operating System :: POSIX
|
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
41
|
+
Requires-Python: >=3.12
|
|
42
|
+
Requires-Dist: httpx>=0.27
|
|
43
|
+
Requires-Dist: pyyaml>=6.0
|
|
44
|
+
Requires-Dist: rich>=13.7
|
|
45
|
+
Requires-Dist: typer>=0.12
|
|
46
|
+
Provides-Extra: dev
|
|
47
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
48
|
+
Requires-Dist: pytest-cov>=4.1; extra == 'dev'
|
|
49
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
50
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
51
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
52
|
+
Description-Content-Type: text/markdown
|
|
53
|
+
|
|
54
|
+
# blobhub-cli
|
|
55
|
+
|
|
56
|
+
The command line interface for [BlobHub](https://blobhub.io/) — the serverless platform for versioned blob
|
|
57
|
+
storage. `blobhub` authenticates against the platform, addresses your orgs, blobs and revisions, and works with
|
|
58
|
+
what those revisions contain, from a terminal or from CI.
|
|
59
|
+
|
|
60
|
+
## How the CLI is organized
|
|
61
|
+
|
|
62
|
+
Everything that is not tied to a particular kind of blob — authentication and profiles, JSON output, error
|
|
63
|
+
reporting — is shared:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
blobhub login | logout | whoami | profile ls | profile use
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Everything else is grouped by **blob domain**, which is the axis the platform itself extends along. Each domain
|
|
70
|
+
adds its own command group and its own manifest types over one shared base:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
blobhub workflow … # workflow blobs — the domain implemented today
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**`workflow` is the first domain and currently the most developed one.** Its flagship capability — deploying a
|
|
77
|
+
revision's code from a real Python package rather than hand-editing it in the visual editor — is described below.
|
|
78
|
+
Further domains slot in as additional command groups without changing anything above this line.
|
|
79
|
+
|
|
80
|
+
## Install
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
pipx install blobhub-cli
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
(or `pip install blobhub-cli` inside a virtualenv). Requires Python 3.12+; installs a `blobhub` binary.
|
|
87
|
+
|
|
88
|
+
## Authenticate
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
blobhub login
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Prompts for the API URL (defaults to `https://api.blobhub.io/v1`) and your API key, verifies it against the
|
|
95
|
+
platform, and stores it as a named profile under `~/.blobhub/credentials.yaml` (`0600`, refused on load if
|
|
96
|
+
looser). Multiple profiles are supported:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
blobhub profile ls # list stored profiles, marking the default
|
|
100
|
+
blobhub profile use staging # switch the default
|
|
101
|
+
blobhub login --profile staging --api-url https://api.staging.blobhub.io/v1
|
|
102
|
+
blobhub whoami # resolved identity, profile, URL, and key scope
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
For CI, skip the credentials file entirely — set `BLOBHUB_API_KEY` (and optionally `BLOBHUB_API_URL`,
|
|
106
|
+
`BLOBHUB_PROFILE`); the environment always wins over a stored profile, and a key from the environment alone
|
|
107
|
+
works with no credentials file present at all.
|
|
108
|
+
|
|
109
|
+
A manifest may name its blob bare (`blob: checkout`) instead of org-qualified. The org then comes from the global
|
|
110
|
+
`--org` option (`blobhub --org acme-corp workflow deploy …`) or from `BLOBHUB_ORG`; with neither, the bare name is
|
|
111
|
+
a manifest error naming all three ways to supply one.
|
|
112
|
+
|
|
113
|
+
## The `workflow` domain
|
|
114
|
+
|
|
115
|
+
A workflow blob's revision holds definitions whose `logic.code` components normally have their code typed into
|
|
116
|
+
the browser, where it cannot be imported, unit-tested, linted, reviewed or version-controlled. The `workflow`
|
|
117
|
+
command group closes that gap: you keep the code as an ordinary Python package, and the CLI compiles it into the
|
|
118
|
+
flat, import-less namespace the platform's sandbox actually executes.
|
|
119
|
+
|
|
120
|
+
### A worked example: deploying a workflow
|
|
121
|
+
|
|
122
|
+
Say your revision has one workflow definition, alias `checkout_flow`, with a `logic.code` component (`id: c1`)
|
|
123
|
+
whose code should come from a real Python package rather than being typed into the editor:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
my-project/
|
|
127
|
+
├── manifest.yaml
|
|
128
|
+
├── definitions/
|
|
129
|
+
│ └── checkout_flow.json # the definition document — pulled from, or pushed to, the platform
|
|
130
|
+
└── pkg/
|
|
131
|
+
├── entry.py # entry_point
|
|
132
|
+
└── lib/
|
|
133
|
+
└── pricing.py # a local module entry.py imports
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`manifest.yaml`:
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
type: workflow_blob_deployment
|
|
140
|
+
version: "1.0"
|
|
141
|
+
blob: acme-corp/checkout # <org>/<blob>, or a bare <blob> plus --org/BLOBHUB_ORG
|
|
142
|
+
revision: latest # or a pinned revision UUID
|
|
143
|
+
definitions:
|
|
144
|
+
- alias: checkout_flow
|
|
145
|
+
source: definitions/checkout_flow.json
|
|
146
|
+
category: workflow
|
|
147
|
+
components:
|
|
148
|
+
- id: c1
|
|
149
|
+
code:
|
|
150
|
+
base_path: pkg
|
|
151
|
+
entry_point: entry.py
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
First pull the current definition (skip this if you're starting a definition from scratch):
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
blobhub workflow pull -f manifest.yaml
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Compile and deploy:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
blobhub workflow deploy -f manifest.yaml
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`deploy` builds every bound component locally (inlining local imports from `pkg/`, stripping sandbox-pre-bound
|
|
167
|
+
and allowlisted imports, rejecting anything else), uploads only the definitions whose serialized document
|
|
168
|
+
actually changed, and runs the platform's `check_definition` against each workflow definition it uploaded.
|
|
169
|
+
Re-running `deploy` with nothing changed uploads nothing and says so. `--dry-run` on every mutating command
|
|
170
|
+
prints the exact intended calls without performing them; `--json` emits a single machine-readable object instead
|
|
171
|
+
of the rendered output.
|
|
172
|
+
|
|
173
|
+
Other commands in the `workflow` group:
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
blobhub workflow diff -f manifest.yaml # report local/remote differences without writing or uploading
|
|
177
|
+
blobhub workflow build -f manifest.yaml # compile locally only, no network
|
|
178
|
+
blobhub workflow check -f manifest.yaml # re-run check_definition against what's already deployed
|
|
179
|
+
blobhub workflow push -f manifest.yaml # upload verbatim, without the compiler (rarely what you want)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
If a code port was edited in the visual editor since the CLI last built it, `build`/`deploy` refuse to overwrite
|
|
183
|
+
it with `REMOTE_EDIT` (or `UNMANAGED_CODE` for hand-typed code the CLI never built) — pass `--force` to overwrite,
|
|
184
|
+
or reconcile by hand first. See `docs/reference/error-codes.md` for the full catalog of error and advisory codes,
|
|
185
|
+
what each one means, and how to resolve it.
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# blobhub-cli
|
|
2
|
+
|
|
3
|
+
The command line interface for [BlobHub](https://blobhub.io/) — the serverless platform for versioned blob
|
|
4
|
+
storage. `blobhub` authenticates against the platform, addresses your orgs, blobs and revisions, and works with
|
|
5
|
+
what those revisions contain, from a terminal or from CI.
|
|
6
|
+
|
|
7
|
+
## How the CLI is organized
|
|
8
|
+
|
|
9
|
+
Everything that is not tied to a particular kind of blob — authentication and profiles, JSON output, error
|
|
10
|
+
reporting — is shared:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
blobhub login | logout | whoami | profile ls | profile use
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Everything else is grouped by **blob domain**, which is the axis the platform itself extends along. Each domain
|
|
17
|
+
adds its own command group and its own manifest types over one shared base:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
blobhub workflow … # workflow blobs — the domain implemented today
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**`workflow` is the first domain and currently the most developed one.** Its flagship capability — deploying a
|
|
24
|
+
revision's code from a real Python package rather than hand-editing it in the visual editor — is described below.
|
|
25
|
+
Further domains slot in as additional command groups without changing anything above this line.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pipx install blobhub-cli
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
(or `pip install blobhub-cli` inside a virtualenv). Requires Python 3.12+; installs a `blobhub` binary.
|
|
34
|
+
|
|
35
|
+
## Authenticate
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
blobhub login
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Prompts for the API URL (defaults to `https://api.blobhub.io/v1`) and your API key, verifies it against the
|
|
42
|
+
platform, and stores it as a named profile under `~/.blobhub/credentials.yaml` (`0600`, refused on load if
|
|
43
|
+
looser). Multiple profiles are supported:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
blobhub profile ls # list stored profiles, marking the default
|
|
47
|
+
blobhub profile use staging # switch the default
|
|
48
|
+
blobhub login --profile staging --api-url https://api.staging.blobhub.io/v1
|
|
49
|
+
blobhub whoami # resolved identity, profile, URL, and key scope
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For CI, skip the credentials file entirely — set `BLOBHUB_API_KEY` (and optionally `BLOBHUB_API_URL`,
|
|
53
|
+
`BLOBHUB_PROFILE`); the environment always wins over a stored profile, and a key from the environment alone
|
|
54
|
+
works with no credentials file present at all.
|
|
55
|
+
|
|
56
|
+
A manifest may name its blob bare (`blob: checkout`) instead of org-qualified. The org then comes from the global
|
|
57
|
+
`--org` option (`blobhub --org acme-corp workflow deploy …`) or from `BLOBHUB_ORG`; with neither, the bare name is
|
|
58
|
+
a manifest error naming all three ways to supply one.
|
|
59
|
+
|
|
60
|
+
## The `workflow` domain
|
|
61
|
+
|
|
62
|
+
A workflow blob's revision holds definitions whose `logic.code` components normally have their code typed into
|
|
63
|
+
the browser, where it cannot be imported, unit-tested, linted, reviewed or version-controlled. The `workflow`
|
|
64
|
+
command group closes that gap: you keep the code as an ordinary Python package, and the CLI compiles it into the
|
|
65
|
+
flat, import-less namespace the platform's sandbox actually executes.
|
|
66
|
+
|
|
67
|
+
### A worked example: deploying a workflow
|
|
68
|
+
|
|
69
|
+
Say your revision has one workflow definition, alias `checkout_flow`, with a `logic.code` component (`id: c1`)
|
|
70
|
+
whose code should come from a real Python package rather than being typed into the editor:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
my-project/
|
|
74
|
+
├── manifest.yaml
|
|
75
|
+
├── definitions/
|
|
76
|
+
│ └── checkout_flow.json # the definition document — pulled from, or pushed to, the platform
|
|
77
|
+
└── pkg/
|
|
78
|
+
├── entry.py # entry_point
|
|
79
|
+
└── lib/
|
|
80
|
+
└── pricing.py # a local module entry.py imports
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`manifest.yaml`:
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
type: workflow_blob_deployment
|
|
87
|
+
version: "1.0"
|
|
88
|
+
blob: acme-corp/checkout # <org>/<blob>, or a bare <blob> plus --org/BLOBHUB_ORG
|
|
89
|
+
revision: latest # or a pinned revision UUID
|
|
90
|
+
definitions:
|
|
91
|
+
- alias: checkout_flow
|
|
92
|
+
source: definitions/checkout_flow.json
|
|
93
|
+
category: workflow
|
|
94
|
+
components:
|
|
95
|
+
- id: c1
|
|
96
|
+
code:
|
|
97
|
+
base_path: pkg
|
|
98
|
+
entry_point: entry.py
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
First pull the current definition (skip this if you're starting a definition from scratch):
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
blobhub workflow pull -f manifest.yaml
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Compile and deploy:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
blobhub workflow deploy -f manifest.yaml
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`deploy` builds every bound component locally (inlining local imports from `pkg/`, stripping sandbox-pre-bound
|
|
114
|
+
and allowlisted imports, rejecting anything else), uploads only the definitions whose serialized document
|
|
115
|
+
actually changed, and runs the platform's `check_definition` against each workflow definition it uploaded.
|
|
116
|
+
Re-running `deploy` with nothing changed uploads nothing and says so. `--dry-run` on every mutating command
|
|
117
|
+
prints the exact intended calls without performing them; `--json` emits a single machine-readable object instead
|
|
118
|
+
of the rendered output.
|
|
119
|
+
|
|
120
|
+
Other commands in the `workflow` group:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
blobhub workflow diff -f manifest.yaml # report local/remote differences without writing or uploading
|
|
124
|
+
blobhub workflow build -f manifest.yaml # compile locally only, no network
|
|
125
|
+
blobhub workflow check -f manifest.yaml # re-run check_definition against what's already deployed
|
|
126
|
+
blobhub workflow push -f manifest.yaml # upload verbatim, without the compiler (rarely what you want)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
If a code port was edited in the visual editor since the CLI last built it, `build`/`deploy` refuse to overwrite
|
|
130
|
+
it with `REMOTE_EDIT` (or `UNMANAGED_CODE` for hand-typed code the CLI never built) — pass `--force` to overwrite,
|
|
131
|
+
or reconcile by hand first. See `docs/reference/error-codes.md` for the full catalog of error and advisory codes,
|
|
132
|
+
what each one means, and how to resolve it.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "blobhub-cli"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "BlobHub command line interface — work with blobs, revisions, and their contents from the terminal"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { file = "LICENSE" }
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
authors = [{ name = "BlobHub", email = "developers@blobhub.io" }]
|
|
9
|
+
keywords = ["blobhub", "cli", "command-line", "blobs", "versioning", "workflows", "automation"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Operating System :: POSIX",
|
|
15
|
+
"Operating System :: MacOS",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Topic :: Software Development :: Build Tools",
|
|
19
|
+
]
|
|
20
|
+
dependencies = ["typer>=0.12", "rich>=13.7", "httpx>=0.27", "pyyaml>=6.0"]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://blobhub.io/"
|
|
24
|
+
Documentation = "https://docs.blobhub.io/"
|
|
25
|
+
Source = "https://github.com/blobhubio/blobhub-cli"
|
|
26
|
+
Issues = "https://github.com/blobhubio/blobhub-cli/issues"
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
blobhub = "blobhub_cli.cli.app:main"
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = ["pytest>=8.0", "pytest-cov>=4.1", "ruff>=0.6", "build>=1.2", "twine>=5.0"]
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["hatchling"]
|
|
36
|
+
build-backend = "hatchling.build"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.version]
|
|
39
|
+
path = "src/blobhub_cli/__init__.py"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["src/blobhub_cli"]
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.sdist]
|
|
45
|
+
# Anchored with a leading slash. Hatchling matches gitignore-style, so a bare `README.md` also matched
|
|
46
|
+
# `tests_online/README.md` and shipped the online tier's setup notes to everyone installing the sdist.
|
|
47
|
+
include = ["/src/blobhub_cli", "/README.md", "/LICENSE", "/pyproject.toml"]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
pythonpath = ["src", "."]
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 120
|
|
55
|
+
target-version = "py312"
|
|
56
|
+
|
|
57
|
+
[tool.ruff.lint]
|
|
58
|
+
select = ["E", "F", "W", "I", "UP", "B", "BLE", "ARG", "SIM", "RUF"]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""BlobHub CLI package root.
|
|
2
|
+
|
|
3
|
+
Contract: this module is the sole source of the package version — hatchling reads
|
|
4
|
+
`__version__` from here via `[tool.hatch.version] path` in pyproject.toml.
|
|
5
|
+
Invariant: no other file in this package defines `__version__`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Process entry point for `python -m blobhub_cli`.
|
|
2
|
+
|
|
3
|
+
Contract: invoking this module runs the CLI the same way the installed `blobhub`
|
|
4
|
+
console script does.
|
|
5
|
+
Invariant: holds no logic of its own — it only delegates to `cli.app.main`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from blobhub_cli.cli.app import main
|
|
9
|
+
|
|
10
|
+
if __name__ == "__main__":
|
|
11
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""HTTP client and error hierarchy for talking to the BlobHub platform.
|
|
2
|
+
|
|
3
|
+
Contract: `client.Client` is the only object in this package (and the only one in
|
|
4
|
+
`blobhub_cli`) permitted to perform network I/O; every other module reaches the platform
|
|
5
|
+
through it.
|
|
6
|
+
Invariant: nothing in this package ever logs, echoes, or includes an API key in an
|
|
7
|
+
exception message, output line, or `--json` payload.
|
|
8
|
+
"""
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Synchronous HTTP client for the BlobHub API — the only module in this package that performs network I/O.
|
|
2
|
+
|
|
3
|
+
Contract: `get`, `query`, `command`, and `raw_query` route every response through `_handle`,
|
|
4
|
+
which maps HTTP status and the body's `status` field onto the `api.errors` hierarchy;
|
|
5
|
+
`raw_query` alone skips the status gate, and `probe` skips both the gate and the retry loop.
|
|
6
|
+
`query`/`command`/`raw_query` take a required keyword-only `engine` -- the engine a data request
|
|
7
|
+
targets is the caller's to supply, never this class's to assume, because one command can
|
|
8
|
+
legitimately span both: a scheduler `deploy` writes schedules against `scheduler_blobhub` and
|
|
9
|
+
validates its targets with `workflow_blobhub` calls against the target revision, so a client
|
|
10
|
+
fixed to one engine could not do that.
|
|
11
|
+
Invariant: `Content-Type: application/json` is sent on every request, and the API key is
|
|
12
|
+
never logged, echoed, or included in any exception message. This module is the only one that
|
|
13
|
+
performs I/O against the BlobHub API -- the single sanctioned exception elsewhere is `doctor`'s
|
|
14
|
+
PyPI version probe, which must NOT go through this class precisely because this class attaches
|
|
15
|
+
`X-API-Key` to every request and the key must never reach a third-party host.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import random
|
|
19
|
+
import time
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
|
|
23
|
+
from blobhub_cli.api.errors import (
|
|
24
|
+
ApiAuthError,
|
|
25
|
+
ApiCommandError,
|
|
26
|
+
ApiNetworkError,
|
|
27
|
+
ApiRateLimited,
|
|
28
|
+
ApiTransientError,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
WORKFLOW_ENGINE = "workflow_blobhub"
|
|
32
|
+
SCHEDULER_ENGINE = "scheduler_blobhub"
|
|
33
|
+
|
|
34
|
+
_TRANSIENT = (ApiRateLimited, ApiTransientError, ApiNetworkError)
|
|
35
|
+
_ATTEMPTS = 3
|
|
36
|
+
_BASE_DELAY = 0.5
|
|
37
|
+
_MAX_DELAY = 8.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Client:
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
api_key: str,
|
|
44
|
+
base_url: str,
|
|
45
|
+
*,
|
|
46
|
+
transport: httpx.BaseTransport | None = None,
|
|
47
|
+
timeout: float = 30.0,
|
|
48
|
+
sleep=time.sleep,
|
|
49
|
+
) -> None:
|
|
50
|
+
self._sleep = sleep
|
|
51
|
+
self._http = httpx.Client(
|
|
52
|
+
base_url=base_url,
|
|
53
|
+
headers={"X-API-Key": api_key, "Content-Type": "application/json"},
|
|
54
|
+
transport=transport,
|
|
55
|
+
timeout=timeout,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def __enter__(self) -> "Client":
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def __exit__(self, *exc_info: object) -> None:
|
|
62
|
+
self._http.close()
|
|
63
|
+
|
|
64
|
+
def get(self, path: str) -> dict:
|
|
65
|
+
return self._send("GET", path, gate_status=True)
|
|
66
|
+
|
|
67
|
+
def probe(self, path: str = "/users/me") -> tuple[int, float]:
|
|
68
|
+
"""One unretried request, returning (status_code, elapsed_ms) without gating the body.
|
|
69
|
+
|
|
70
|
+
`doctor` uses this to prove reachability independently of credentials: every route needs
|
|
71
|
+
auth, so a 401 is a perfectly good proof that the host answered. Deliberately unretried --
|
|
72
|
+
a diagnostic wants a fast honest answer, not three backoffs.
|
|
73
|
+
"""
|
|
74
|
+
start = time.monotonic()
|
|
75
|
+
try:
|
|
76
|
+
resp = self._http.request("GET", path)
|
|
77
|
+
except httpx.TransportError as exc:
|
|
78
|
+
raise ApiNetworkError(str(exc)) from exc
|
|
79
|
+
return resp.status_code, (time.monotonic() - start) * 1000
|
|
80
|
+
|
|
81
|
+
def query(self, revision_id: str, command: str, *, engine: str, **args: object) -> dict:
|
|
82
|
+
return self._data_request(revision_id, "query", command, engine=engine, gate_status=True, **args)
|
|
83
|
+
|
|
84
|
+
def command(self, revision_id: str, command: str, *, engine: str, **args: object) -> dict:
|
|
85
|
+
return self._data_request(revision_id, "command", command, engine=engine, gate_status=True, **args)
|
|
86
|
+
|
|
87
|
+
def raw_query(self, revision_id: str, command: str, *, engine: str, **args: object) -> dict:
|
|
88
|
+
return self._data_request(revision_id, "query", command, engine=engine, gate_status=False, **args)
|
|
89
|
+
|
|
90
|
+
def _data_request(
|
|
91
|
+
self, revision_id: str, channel: str, command: str, *, engine: str, gate_status: bool, **args: object
|
|
92
|
+
) -> dict:
|
|
93
|
+
path = f"/revisions/{revision_id}/data/{channel}"
|
|
94
|
+
body = {"engine": engine, "command": command, **args}
|
|
95
|
+
return self._send("POST", path, gate_status=gate_status, json=body)
|
|
96
|
+
|
|
97
|
+
def _send(self, method: str, path: str, *, gate_status: bool, **kwargs: object) -> dict:
|
|
98
|
+
last_error: Exception
|
|
99
|
+
for attempt in range(_ATTEMPTS):
|
|
100
|
+
try:
|
|
101
|
+
resp = self._http.request(method, path, **kwargs)
|
|
102
|
+
except httpx.TransportError as exc:
|
|
103
|
+
last_error = ApiNetworkError(str(exc))
|
|
104
|
+
else:
|
|
105
|
+
try:
|
|
106
|
+
return self._handle(resp, gate_status=gate_status)
|
|
107
|
+
except _TRANSIENT as exc:
|
|
108
|
+
last_error = exc
|
|
109
|
+
if attempt < _ATTEMPTS - 1:
|
|
110
|
+
self._sleep(random.random() * min(_MAX_DELAY, _BASE_DELAY * 2**attempt))
|
|
111
|
+
raise last_error
|
|
112
|
+
|
|
113
|
+
def _handle(self, resp: httpx.Response, *, gate_status: bool) -> dict:
|
|
114
|
+
if resp.status_code in (401, 403):
|
|
115
|
+
raise ApiAuthError(f"auth failed: HTTP {resp.status_code}")
|
|
116
|
+
if resp.status_code == 429:
|
|
117
|
+
raise ApiRateLimited("HTTP 429")
|
|
118
|
+
if resp.status_code >= 500:
|
|
119
|
+
raise ApiTransientError(f"HTTP {resp.status_code}")
|
|
120
|
+
try:
|
|
121
|
+
data = resp.json()
|
|
122
|
+
except ValueError as exc:
|
|
123
|
+
raise ApiTransientError(f"non-JSON response: HTTP {resp.status_code}") from exc
|
|
124
|
+
if gate_status and data.get("status") != "success":
|
|
125
|
+
raise ApiCommandError(data.get("error", "unknown"), data.get("message", ""))
|
|
126
|
+
return data
|