stml-cli 0.1.6__tar.gz → 0.1.8__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.
- {stml_cli-0.1.6 → stml_cli-0.1.8}/PKG-INFO +1 -1
- {stml_cli-0.1.6 → stml_cli-0.1.8}/pyproject.toml +5 -0
- {stml_cli-0.1.6 → stml_cli-0.1.8}/stml_cli/__init__.py +1 -1
- {stml_cli-0.1.6 → stml_cli-0.1.8}/stml_cli/commands.py +78 -3
- {stml_cli-0.1.6 → stml_cli-0.1.8}/stml_cli/paths.py +3 -2
- {stml_cli-0.1.6 → stml_cli-0.1.8}/stml_cli.egg-info/PKG-INFO +1 -1
- {stml_cli-0.1.6 → stml_cli-0.1.8}/stml_cli.egg-info/SOURCES.txt +1 -0
- stml_cli-0.1.8/tests/test_init.py +42 -0
- {stml_cli-0.1.6 → stml_cli-0.1.8}/tests/test_paths.py +12 -0
- {stml_cli-0.1.6 → stml_cli-0.1.8}/README.md +0 -0
- {stml_cli-0.1.6 → stml_cli-0.1.8}/setup.cfg +0 -0
- {stml_cli-0.1.6 → stml_cli-0.1.8}/stml_cli/__main__.py +0 -0
- {stml_cli-0.1.6 → stml_cli-0.1.8}/stml_cli/config.py +0 -0
- {stml_cli-0.1.6 → stml_cli-0.1.8}/stml_cli/http.py +0 -0
- {stml_cli-0.1.6 → stml_cli-0.1.8}/stml_cli/oauth.py +0 -0
- {stml_cli-0.1.6 → stml_cli-0.1.8}/stml_cli/packaging.py +0 -0
- {stml_cli-0.1.6 → stml_cli-0.1.8}/stml_cli/refs.py +0 -0
- {stml_cli-0.1.6 → stml_cli-0.1.8}/stml_cli.egg-info/dependency_links.txt +0 -0
- {stml_cli-0.1.6 → stml_cli-0.1.8}/stml_cli.egg-info/entry_points.txt +0 -0
- {stml_cli-0.1.6 → stml_cli-0.1.8}/stml_cli.egg-info/requires.txt +0 -0
- {stml_cli-0.1.6 → stml_cli-0.1.8}/stml_cli.egg-info/top_level.txt +0 -0
- {stml_cli-0.1.6 → stml_cli-0.1.8}/tests/test_disambiguation.py +0 -0
- {stml_cli-0.1.6 → stml_cli-0.1.8}/tests/test_refs.py +0 -0
- {stml_cli-0.1.6 → stml_cli-0.1.8}/tests/test_whoami_expiry.py +0 -0
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import os
|
|
11
11
|
|
|
12
|
-
__version__ = "0.1.
|
|
12
|
+
__version__ = "0.1.8"
|
|
13
13
|
|
|
14
14
|
# TLS trust: framework/python.org builds have no OpenSSL CA path, so a real
|
|
15
15
|
# https backend fails with "unable to get local issuer certificate". certifi's
|
|
@@ -152,6 +152,16 @@ def cmd_pull(args) -> None:
|
|
|
152
152
|
_write_link(dest, args.backend, workspace, app)
|
|
153
153
|
tail = f" (+ {asset_count} asset(s))" if asset_count else ""
|
|
154
154
|
_log(f"✓ Pulled {len(written)} file(s) from {result.get('app', app)} → {dest}{tail}")
|
|
155
|
+
if result.get("generated_manifest"):
|
|
156
|
+
# The app's overlay had no manifest — the backend reconstructed one from
|
|
157
|
+
# the platform's live state (flow rows, dependency pins, provenance).
|
|
158
|
+
_log(" ℹ pyproject.toml was generated from the app's platform state — "
|
|
159
|
+
"review it (name/version/issuer-org) before `stml publish`.")
|
|
160
|
+
if result.get("base_files"):
|
|
161
|
+
# Attached app: files the app never forked come from its base library
|
|
162
|
+
# version, so the folder is the complete effective tree.
|
|
163
|
+
_log(f" ℹ {result['base_files']} file(s) came from the app's base "
|
|
164
|
+
"library (never forked in the app).")
|
|
155
165
|
|
|
156
166
|
|
|
157
167
|
def cmd_push(args) -> None:
|
|
@@ -305,6 +315,7 @@ def cmd_init(args) -> None:
|
|
|
305
315
|
raise SystemExit(f"{target} exists and is not empty")
|
|
306
316
|
target.mkdir(parents=True, exist_ok=True)
|
|
307
317
|
name = args.name or target.name
|
|
318
|
+
pkg_name = name.replace("-", "_")
|
|
308
319
|
(target / "pyproject.toml").write_text(
|
|
309
320
|
f'''[project]
|
|
310
321
|
name = "{name}"
|
|
@@ -316,15 +327,79 @@ dependencies = []
|
|
|
316
327
|
[tool.stml]
|
|
317
328
|
requires-runtime = ">=0.1"
|
|
318
329
|
issuer-org = "" # set to your org slug
|
|
319
|
-
exports-flows = [
|
|
330
|
+
exports-flows = [
|
|
331
|
+
{{ name = "hello", entrypoint = "{pkg_name}.hello:app" }},
|
|
332
|
+
]
|
|
320
333
|
depends-on = []
|
|
334
|
+
|
|
335
|
+
[dependency-groups]
|
|
336
|
+
dev = [
|
|
337
|
+
"pytest",
|
|
338
|
+
]
|
|
339
|
+
|
|
340
|
+
[tool.pytest.ini_options]
|
|
341
|
+
pythonpath = ["src"]
|
|
321
342
|
''',
|
|
322
343
|
encoding="utf-8",
|
|
323
344
|
)
|
|
324
|
-
|
|
325
|
-
|
|
345
|
+
# src layout — the shape `stml pull` writes and deployment-version 2
|
|
346
|
+
# expects (flows ship under src/); keeps init/pull/push round-trips stable.
|
|
347
|
+
pkg = target / "src" / pkg_name
|
|
348
|
+
pkg.mkdir(parents=True)
|
|
326
349
|
(pkg / "__init__.py").write_text('"""Library entry point."""\n', encoding="utf-8")
|
|
350
|
+
(pkg / "hello.py").write_text(
|
|
351
|
+
f'''"""A minimal runnable flow — replace its steps with your own and keep
|
|
352
|
+
`exports-flows` in pyproject.toml pointing at each flow's `module:app`."""
|
|
353
|
+
|
|
354
|
+
from stml.runtime import App
|
|
355
|
+
|
|
356
|
+
app = App("hello")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@app.step(name="Greet", description="Says hello.")
|
|
360
|
+
def greet() -> dict:
|
|
361
|
+
return {{"message": "Hello from {name}!"}}
|
|
362
|
+
''',
|
|
363
|
+
encoding="utf-8",
|
|
364
|
+
)
|
|
327
365
|
(target / "README.md").write_text(f"# {name}\n", encoding="utf-8")
|
|
366
|
+
(target / "AGENTS.md").write_text(
|
|
367
|
+
f'''# `{name}` — usage for the assistant
|
|
368
|
+
|
|
369
|
+
<What this library does, who installs it, and how its flows are meant to be
|
|
370
|
+
run — write it for the AI assistant working on or with this library.>
|
|
371
|
+
|
|
372
|
+
## Layout
|
|
373
|
+
|
|
374
|
+
- `src/{pkg_name}/` — the package. Flows live here and are listed in
|
|
375
|
+
`pyproject.toml` under `[tool.stml] exports-flows` as `module:app`.
|
|
376
|
+
- `tests/` — local-only: `stml push` and `stml publish` both skip it.
|
|
377
|
+
Run with `uv run pytest`.
|
|
378
|
+
''',
|
|
379
|
+
encoding="utf-8",
|
|
380
|
+
)
|
|
381
|
+
(target / ".gitignore").write_text(
|
|
382
|
+
".venv/\n__pycache__/\n*.pyc\n.stml/\n.pytest_cache/\n.ruff_cache/\n",
|
|
383
|
+
encoding="utf-8",
|
|
384
|
+
)
|
|
385
|
+
# tests/ is LOCAL-ONLY by convention: push and publish both skip it
|
|
386
|
+
# (paths._EXCLUDE_DIRS / packaging.EXCLUDE_DIRS), so scaffold it with a
|
|
387
|
+
# stub that says so — runnable code belongs in the package, never here.
|
|
388
|
+
tests_dir = target / "tests"
|
|
389
|
+
tests_dir.mkdir()
|
|
390
|
+
(tests_dir / "__init__.py").write_text("", encoding="utf-8")
|
|
391
|
+
(tests_dir / "test_smoke.py").write_text(
|
|
392
|
+
f'''"""tests/ is local-only: `stml push` and `stml publish` both skip this
|
|
393
|
+
folder — put runnable code in the package, never in here."""
|
|
394
|
+
|
|
395
|
+
import {pkg_name}
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_import():
|
|
399
|
+
assert {pkg_name}.__doc__
|
|
400
|
+
''',
|
|
401
|
+
encoding="utf-8",
|
|
402
|
+
)
|
|
328
403
|
_log(f"✓ Scaffolded {target}")
|
|
329
404
|
|
|
330
405
|
|
|
@@ -47,9 +47,10 @@ def _is_packaging_path(rel: str) -> bool:
|
|
|
47
47
|
return True
|
|
48
48
|
return False
|
|
49
49
|
|
|
50
|
-
# Local-only artifacts that must never be pushed into the overlay.
|
|
50
|
+
# Local-only artifacts that must never be pushed into the overlay. `tests`
|
|
51
|
+
# matches packaging.EXCLUDE_DIRS: unit tests are test-only, never shipped.
|
|
51
52
|
_EXCLUDE_DIRS = {
|
|
52
|
-
".git", "__pycache__", ".venv", "venv", ".stml",
|
|
53
|
+
".git", "__pycache__", ".venv", "venv", ".stml", "tests",
|
|
53
54
|
"node_modules", ".mypy_cache", ".ruff_cache", ".pytest_cache",
|
|
54
55
|
}
|
|
55
56
|
_EXCLUDE_NAMES = {".DS_Store"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from types import SimpleNamespace
|
|
3
|
+
|
|
4
|
+
from stml_cli import commands, paths
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_init_scaffolds_src_layout_lib(tmp_path: Path):
|
|
8
|
+
target = tmp_path / "my-lib"
|
|
9
|
+
commands.cmd_init(SimpleNamespace(dir=str(target), name=None))
|
|
10
|
+
|
|
11
|
+
# src layout with a runnable example flow, exported in pyproject.
|
|
12
|
+
assert (target / "src" / "my_lib" / "__init__.py").is_file()
|
|
13
|
+
assert (target / "src" / "my_lib" / "hello.py").is_file()
|
|
14
|
+
pyproject = (target / "pyproject.toml").read_text()
|
|
15
|
+
assert 'entrypoint = "my_lib.hello:app"' in pyproject
|
|
16
|
+
# pytest works out of the box: dev dep + src on the test pythonpath.
|
|
17
|
+
assert "pytest" in pyproject and 'pythonpath = ["src"]' in pyproject
|
|
18
|
+
assert (target / "AGENTS.md").is_file()
|
|
19
|
+
assert (target / ".gitignore").is_file()
|
|
20
|
+
|
|
21
|
+
# tests/ is scaffolded but local-only: the stub imports the package name
|
|
22
|
+
# and push (read_tree) must not pick the folder up.
|
|
23
|
+
stub = (target / "tests" / "test_smoke.py").read_text()
|
|
24
|
+
assert "import my_lib" in stub
|
|
25
|
+
keys = {f["path"] for f in paths.read_tree(target)}
|
|
26
|
+
assert "my_lib/hello.py" in keys # src/ stripped to flat overlay keys
|
|
27
|
+
assert not any(k.startswith("tests/") for k in keys)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_init_scaffolded_smoke_test_passes(tmp_path: Path):
|
|
31
|
+
"""The generated tests/test_smoke.py must actually pass in the scaffold."""
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
|
|
35
|
+
target = tmp_path / "demo-lib"
|
|
36
|
+
commands.cmd_init(SimpleNamespace(dir=str(target), name=None))
|
|
37
|
+
res = subprocess.run(
|
|
38
|
+
[sys.executable, "-m", "pytest", "tests/", "-q", "-p", "no:cacheprovider",
|
|
39
|
+
"--rootdir", str(target), "-o", 'pythonpath=src'],
|
|
40
|
+
cwd=target, capture_output=True, text=True,
|
|
41
|
+
)
|
|
42
|
+
assert res.returncode == 0, res.stdout + res.stderr
|
|
@@ -114,6 +114,18 @@ def test_local_junk_is_excluded(tmp_path: Path):
|
|
|
114
114
|
assert keys == {"flows/x.py"} # .git, __pycache__, .stml, *.pyc all dropped
|
|
115
115
|
|
|
116
116
|
|
|
117
|
+
def test_tests_dir_is_never_pushed(tmp_path: Path):
|
|
118
|
+
"""tests/ is test-only and never shipped — push skips it like publish does
|
|
119
|
+
(packaging.EXCLUDE_DIRS), both top-level and under src/."""
|
|
120
|
+
paths.write_tree(tmp_path, [{"path": "flows/x.py", "content": "x=1\n"}])
|
|
121
|
+
(tmp_path / "tests").mkdir()
|
|
122
|
+
(tmp_path / "tests" / "test_x.py").write_text("def test_x(): pass\n")
|
|
123
|
+
(tmp_path / "src" / "tests").mkdir()
|
|
124
|
+
(tmp_path / "src" / "tests" / "test_y.py").write_text("def test_y(): pass\n")
|
|
125
|
+
keys = {f["path"] for f in paths.read_tree(tmp_path)}
|
|
126
|
+
assert keys == {"flows/x.py"}
|
|
127
|
+
|
|
128
|
+
|
|
117
129
|
def test_assets_dir_is_never_pushed(tmp_path: Path):
|
|
118
130
|
"""Top-level assets/ is bucket-backed binary media (written by pull) — push
|
|
119
131
|
must skip it (a .xlsx would crash read_text) but publish ships it from disk."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|