ream-artifacts 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.
Files changed (108) hide show
  1. ream_artifacts-0.1.0/PKG-INFO +137 -0
  2. ream_artifacts-0.1.0/README.md +117 -0
  3. ream_artifacts-0.1.0/pyproject.toml +80 -0
  4. ream_artifacts-0.1.0/pyproject.toml.orig +109 -0
  5. ream_artifacts-0.1.0/src/artifacts/__init__.py +91 -0
  6. ream_artifacts-0.1.0/src/artifacts/py.typed +0 -0
  7. ream_artifacts-0.1.0/src/ream/__init__.py +0 -0
  8. ream_artifacts-0.1.0/src/ream/admin.py +698 -0
  9. ream_artifacts-0.1.0/src/ream/anchors.py +672 -0
  10. ream_artifacts-0.1.0/src/ream/api.py +3779 -0
  11. ream_artifacts-0.1.0/src/ream/app.py +332 -0
  12. ream_artifacts-0.1.0/src/ream/brand/rooted-lg-dk.svg +12 -0
  13. ream_artifacts-0.1.0/src/ream/brand/rooted-lg-lt.svg +12 -0
  14. ream_artifacts-0.1.0/src/ream/brand/rooted-lg-mono.svg +12 -0
  15. ream_artifacts-0.1.0/src/ream/brand/rooted-sm-dk.svg +11 -0
  16. ream_artifacts-0.1.0/src/ream/brand/rooted-sm-lt.svg +11 -0
  17. ream_artifacts-0.1.0/src/ream/brand/rooted-sm-mono.svg +11 -0
  18. ream_artifacts-0.1.0/src/ream/cli.py +185 -0
  19. ream_artifacts-0.1.0/src/ream/client.py +2574 -0
  20. ream_artifacts-0.1.0/src/ream/clientlevel.py +46 -0
  21. ream_artifacts-0.1.0/src/ream/comments.py +2073 -0
  22. ream_artifacts-0.1.0/src/ream/connect.py +347 -0
  23. ream_artifacts-0.1.0/src/ream/connect_cli.py +222 -0
  24. ream_artifacts-0.1.0/src/ream/content.py +1322 -0
  25. ream_artifacts-0.1.0/src/ream/csp.py +234 -0
  26. ream_artifacts-0.1.0/src/ream/db.py +1190 -0
  27. ream_artifacts-0.1.0/src/ream/decide.py +2727 -0
  28. ream_artifacts-0.1.0/src/ream/decisions.py +2901 -0
  29. ream_artifacts-0.1.0/src/ream/design.py +196 -0
  30. ream_artifacts-0.1.0/src/ream/email_share.py +202 -0
  31. ream_artifacts-0.1.0/src/ream/envnames.py +51 -0
  32. ream_artifacts-0.1.0/src/ream/events.py +694 -0
  33. ream_artifacts-0.1.0/src/ream/folders.py +252 -0
  34. ream_artifacts-0.1.0/src/ream/formanswer.py +537 -0
  35. ream_artifacts-0.1.0/src/ream/formrecord.py +313 -0
  36. ream_artifacts-0.1.0/src/ream/forms.py +830 -0
  37. ream_artifacts-0.1.0/src/ream/forms_ui.py +396 -0
  38. ream_artifacts-0.1.0/src/ream/frame_pass.py +106 -0
  39. ream_artifacts-0.1.0/src/ream/gallery.py +7769 -0
  40. ream_artifacts-0.1.0/src/ream/ggscan.py +243 -0
  41. ream_artifacts-0.1.0/src/ream/grants.py +90 -0
  42. ream_artifacts-0.1.0/src/ream/holds.py +175 -0
  43. ream_artifacts-0.1.0/src/ream/hosts.py +103 -0
  44. ream_artifacts-0.1.0/src/ream/idempotency.py +110 -0
  45. ream_artifacts-0.1.0/src/ream/ids.py +7 -0
  46. ream_artifacts-0.1.0/src/ream/image_store.py +343 -0
  47. ream_artifacts-0.1.0/src/ream/images.py +439 -0
  48. ream_artifacts-0.1.0/src/ream/inbox.py +246 -0
  49. ream_artifacts-0.1.0/src/ream/keys.py +373 -0
  50. ream_artifacts-0.1.0/src/ream/listening.py +373 -0
  51. ream_artifacts-0.1.0/src/ream/logo.py +56 -0
  52. ream_artifacts-0.1.0/src/ream/mail.py +119 -0
  53. ream_artifacts-0.1.0/src/ream/maintenance.py +158 -0
  54. ream_artifacts-0.1.0/src/ream/markdown.py +165 -0
  55. ream_artifacts-0.1.0/src/ream/mcp.py +1193 -0
  56. ream_artifacts-0.1.0/src/ream/mcp_apps.py +371 -0
  57. ream_artifacts-0.1.0/src/ream/mcp_clients.py +508 -0
  58. ream_artifacts-0.1.0/src/ream/mcp_headers.py +108 -0
  59. ream_artifacts-0.1.0/src/ream/mcp_tools.py +1220 -0
  60. ream_artifacts-0.1.0/src/ream/migrate.py +515 -0
  61. ream_artifacts-0.1.0/src/ream/news.py +295 -0
  62. ream_artifacts-0.1.0/src/ream/nudge.py +251 -0
  63. ream_artifacts-0.1.0/src/ream/oauth.py +803 -0
  64. ream_artifacts-0.1.0/src/ream/oidc.py +555 -0
  65. ream_artifacts-0.1.0/src/ream/onboarding.py +1142 -0
  66. ream_artifacts-0.1.0/src/ream/owner.py +132 -0
  67. ream_artifacts-0.1.0/src/ream/packing.py +124 -0
  68. ream_artifacts-0.1.0/src/ream/pagediff.py +1257 -0
  69. ream_artifacts-0.1.0/src/ream/partner.py +680 -0
  70. ream_artifacts-0.1.0/src/ream/partner_in.py +977 -0
  71. ream_artifacts-0.1.0/src/ream/pickup.py +10 -0
  72. ream_artifacts-0.1.0/src/ream/plugin_api.py +547 -0
  73. ream_artifacts-0.1.0/src/ream/plugin_cli.py +414 -0
  74. ream_artifacts-0.1.0/src/ream/py.typed +0 -0
  75. ream_artifacts-0.1.0/src/ream/ratelimit.py +137 -0
  76. ream_artifacts-0.1.0/src/ream/reach.py +75 -0
  77. ream_artifacts-0.1.0/src/ream/receipts.py +332 -0
  78. ream_artifacts-0.1.0/src/ream/rename.py +343 -0
  79. ream_artifacts-0.1.0/src/ream/repack.py +224 -0
  80. ream_artifacts-0.1.0/src/ream/republish.py +250 -0
  81. ream_artifacts-0.1.0/src/ream/review.py +213 -0
  82. ream_artifacts-0.1.0/src/ream/scan.py +924 -0
  83. ream_artifacts-0.1.0/src/ream/scanners.py +183 -0
  84. ream_artifacts-0.1.0/src/ream/schema.sql +1329 -0
  85. ream_artifacts-0.1.0/src/ream/schema_sqlite.sql +287 -0
  86. ream_artifacts-0.1.0/src/ream/secret_shapes.txt +129 -0
  87. ream_artifacts-0.1.0/src/ream/sections.py +679 -0
  88. ream_artifacts-0.1.0/src/ream/selfupdate.py +721 -0
  89. ream_artifacts-0.1.0/src/ream/serve.py +196 -0
  90. ream_artifacts-0.1.0/src/ream/settings.py +719 -0
  91. ream_artifacts-0.1.0/src/ream/sinks.py +716 -0
  92. ream_artifacts-0.1.0/src/ream/stamp.py +92 -0
  93. ream_artifacts-0.1.0/src/ream/store.py +3000 -0
  94. ream_artifacts-0.1.0/src/ream/templates/_shared.css +136 -0
  95. ream_artifacts-0.1.0/src/ream/templates/comparison.html +299 -0
  96. ream_artifacts-0.1.0/src/ream/templates/decision-request.html +309 -0
  97. ream_artifacts-0.1.0/src/ream/templates/design.html +329 -0
  98. ream_artifacts-0.1.0/src/ream/templates/explainer.html +323 -0
  99. ream_artifacts-0.1.0/src/ream/templates/implementation-plan.html +288 -0
  100. ream_artifacts-0.1.0/src/ream/templates/incident.html +321 -0
  101. ream_artifacts-0.1.0/src/ream/templates/intent.html +354 -0
  102. ream_artifacts-0.1.0/src/ream/templates/project-status.html +328 -0
  103. ream_artifacts-0.1.0/src/ream/templates.py +127 -0
  104. ream_artifacts-0.1.0/src/ream/text.py +335 -0
  105. ream_artifacts-0.1.0/src/ream/textvars.py +281 -0
  106. ream_artifacts-0.1.0/src/ream/thumbs.py +496 -0
  107. ream_artifacts-0.1.0/src/ream/traps.py +229 -0
  108. ream_artifacts-0.1.0/src/ream/webhook.py +513 -0
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.3
2
+ Name: ream-artifacts
3
+ Version: 0.1.0
4
+ Summary: A self-hosted artifacts publisher: agents publish HTML pages over a keyed API; Jeremy views them in an Authelia-gated gallery.
5
+ Author: Jeremy Brown
6
+ Author-email: Jeremy Brown <jeremy@tenfourty.com>
7
+ Requires-Dist: cryptography>=46 ; extra == 'server'
8
+ Requires-Dist: fastapi>=0.141.1 ; extra == 'server'
9
+ Requires-Dist: html5lib>=1.1 ; extra == 'server'
10
+ Requires-Dist: markdown-it-py>=4 ; extra == 'server'
11
+ Requires-Dist: markupsafe>=3.0.3 ; extra == 'server'
12
+ Requires-Dist: mdit-py-plugins>=0.5 ; extra == 'server'
13
+ Requires-Dist: psycopg[binary]>=3.3.6 ; extra == 'server'
14
+ Requires-Dist: python-multipart>=0.0.32 ; extra == 'server'
15
+ Requires-Dist: uvicorn>=0.53.0 ; extra == 'server'
16
+ Requires-Dist: zstandard>=0.25 ; extra == 'server'
17
+ Requires-Python: >=3.12
18
+ Provides-Extra: server
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Ream (formerly artifacts)
22
+
23
+ A self-hosted page publisher: agents publish an HTML
24
+ page over a keyed API and get back a stable URL. A person views published pages
25
+ in a gallery behind a sign-in; each page is served from its own
26
+ `<id>.<content domain>` hostname behind a capability token in the URL.
27
+
28
+ ## Development
29
+
30
+ ```bash
31
+ uv run pytest # run tests
32
+ uv run ruff check src tests # lint
33
+ ```
34
+
35
+ Requires Python 3.12 exactly (pinned in `.python-version`) — a later task's
36
+ test dependency (`pgserver`) has no wheels for 3.13+.
37
+
38
+ ## Run locally
39
+
40
+ ```bash
41
+ export DATABASE_URL=postgresql://... # a real Postgres; migrate() runs on startup
42
+ uv run uvicorn ream.serve:app_from_env --factory --reload
43
+ ```
44
+
45
+ The router is fail-closed: `/healthz` only answers 200 with the gallery's own
46
+ `Host` (the host of `REAM_GALLERY_ORIGIN`; `localhost:8000` on a fresh install)
47
+ and 404 on anything else, so curl it with that Host header, not the
48
+ bare address.
49
+
50
+ ## Mint a key for an agent
51
+
52
+ A token is generated where it will be used and never crosses a terminal, a
53
+ transcript, or a network hop — only its SHA-256 is ever registered:
54
+
55
+ ```bash
56
+ # On the machine (or pod) that will hold the key:
57
+ artifacts-admin new-key --agent my-agent --out <your key file>
58
+
59
+ # Then, with the sha256 that command just printed, on the running pod:
60
+ kubectl -n artifacts exec deploy/artifacts -- artifacts-admin register-key \
61
+ --agent my-agent --sha256 <the printed sha256>
62
+ ```
63
+
64
+ `artifacts-admin revoke-key <key_id>` and `artifacts-admin list-keys` manage
65
+ keys from there; neither ever prints a token.
66
+
67
+ ## The skill
68
+
69
+ One skill, `GET /api/v1/skill.md` (open, no key), in the open SKILL.md format: save it wherever
70
+ your agent keeps skills. It is named `ream` and fires whenever the agent shows or shares something
71
+ beyond a short chat answer (a report, a plan, a chart, a diagram, a comparison), not only on the
72
+ words "page" or "publish". With a key set up it publishes to Ream instead of a host's built-in
73
+ artifacts or canvas panel; with none it falls back to that panel and says why. A page carries a
74
+ question only when the agent is blocked on the person or needs their choice. It names no house, so
75
+ it works for any agent on any host; a deployment's own skill adds only its look and keys.
76
+
77
+ ## Publish a page
78
+
79
+ The command is `ream`, with a verb for each job (`ream publish`, `ream ask`,
80
+ `ream wait`; `ream --help` lists them). The older `artifacts-publish`,
81
+ `artifacts-decide` and the other `artifacts-*` commands keep working for good,
82
+ as do the `ARTIFACTS_*` settings, and `/api/v1/artifacts` answers exactly as
83
+ `/api/v1/pages` does (the same handlers, so a POST to either works).
84
+
85
+ ```bash
86
+ ream publish page.html --title "My page"
87
+ ream publish page.html --update <id> # after editing page.html
88
+ ream publish --list
89
+ ```
90
+
91
+ Where the key comes from, in the order to prefer:
92
+
93
+ ```bash
94
+ # A vault hands it over on a descriptor; no cleartext copy exists. Where keys are
95
+ # vaulted, a wrapper does it (here `artifacts-key`, with your seat's name):
96
+ ARTIFACTS_SEAT=<seat> artifacts-key -- ream publish page.html --title "My page"
97
+
98
+ # Or any store of your own that can open one:
99
+ ream publish --key-fd 3 page.html --title "My page" 3< <(pass show artifacts/key)
100
+
101
+ # Or a file, mode 600 -- it refuses anything looser:
102
+ REAM_KEY_FILE=~/.config/artifacts/key ream publish --list
103
+ ```
104
+
105
+ ## Tidy one up
106
+
107
+ Everything Jeremy can do to a page in the gallery, an agent can do to its own
108
+ pages — his rule, 20 Sep 2026: *"ideally agents can take all the same actions a
109
+ human can."* None of these send the body, so none of them needs the file:
110
+
111
+ ```bash
112
+ ream publish --rename <id> --title "Its real name"
113
+ ream publish --archive <id> # off the wall; nothing is deleted
114
+ ream publish --unarchive <id>
115
+ ream publish --pin <id> # leads the gallery
116
+ ream publish --unpin <id>
117
+ ream publish --duplicate <id> # a copy at a new address
118
+ ```
119
+
120
+ Over HTTP these are `POST /api/v1/pages/{id}/{archive,rename,pin}`, each
121
+ scoped to the calling key's own agent: another seat's page answers 404, and so
122
+ does a page that does not exist — the same 404, so a key cannot be used to
123
+ learn which ids are real.
124
+
125
+ `$REAM_API` (default `https://artifacts.tenfourty.site/api/v1`) and
126
+ `$REAM_KEY_FILE` (default `~/.config/artifacts/key`) point it elsewhere;
127
+ `--key-file` overrides both for one call.
128
+
129
+ ## How deploys happen
130
+
131
+ Every push to `main` runs `.forgejo/workflows/ci.yml`: tests, then an image
132
+ build pushed to the forge's registry as `<repo>:<sha12>`, then a pull request on
133
+ the deployment's own repo that moves its `core.lock` to that image
134
+ (`.forgejo/scripts/core-lock-pr.sh`). That repo builds the deployed image and opens
135
+ the pin PR; production moves when that PR is merged. The runbook is
136
+ `docs/agent/runbooks/image-from-ream-homelab.md`. This repo keeps no deployment
137
+ values: they are in the deployment's own repo.
@@ -0,0 +1,117 @@
1
+ # Ream (formerly artifacts)
2
+
3
+ A self-hosted page publisher: agents publish an HTML
4
+ page over a keyed API and get back a stable URL. A person views published pages
5
+ in a gallery behind a sign-in; each page is served from its own
6
+ `<id>.<content domain>` hostname behind a capability token in the URL.
7
+
8
+ ## Development
9
+
10
+ ```bash
11
+ uv run pytest # run tests
12
+ uv run ruff check src tests # lint
13
+ ```
14
+
15
+ Requires Python 3.12 exactly (pinned in `.python-version`) — a later task's
16
+ test dependency (`pgserver`) has no wheels for 3.13+.
17
+
18
+ ## Run locally
19
+
20
+ ```bash
21
+ export DATABASE_URL=postgresql://... # a real Postgres; migrate() runs on startup
22
+ uv run uvicorn ream.serve:app_from_env --factory --reload
23
+ ```
24
+
25
+ The router is fail-closed: `/healthz` only answers 200 with the gallery's own
26
+ `Host` (the host of `REAM_GALLERY_ORIGIN`; `localhost:8000` on a fresh install)
27
+ and 404 on anything else, so curl it with that Host header, not the
28
+ bare address.
29
+
30
+ ## Mint a key for an agent
31
+
32
+ A token is generated where it will be used and never crosses a terminal, a
33
+ transcript, or a network hop — only its SHA-256 is ever registered:
34
+
35
+ ```bash
36
+ # On the machine (or pod) that will hold the key:
37
+ artifacts-admin new-key --agent my-agent --out <your key file>
38
+
39
+ # Then, with the sha256 that command just printed, on the running pod:
40
+ kubectl -n artifacts exec deploy/artifacts -- artifacts-admin register-key \
41
+ --agent my-agent --sha256 <the printed sha256>
42
+ ```
43
+
44
+ `artifacts-admin revoke-key <key_id>` and `artifacts-admin list-keys` manage
45
+ keys from there; neither ever prints a token.
46
+
47
+ ## The skill
48
+
49
+ One skill, `GET /api/v1/skill.md` (open, no key), in the open SKILL.md format: save it wherever
50
+ your agent keeps skills. It is named `ream` and fires whenever the agent shows or shares something
51
+ beyond a short chat answer (a report, a plan, a chart, a diagram, a comparison), not only on the
52
+ words "page" or "publish". With a key set up it publishes to Ream instead of a host's built-in
53
+ artifacts or canvas panel; with none it falls back to that panel and says why. A page carries a
54
+ question only when the agent is blocked on the person or needs their choice. It names no house, so
55
+ it works for any agent on any host; a deployment's own skill adds only its look and keys.
56
+
57
+ ## Publish a page
58
+
59
+ The command is `ream`, with a verb for each job (`ream publish`, `ream ask`,
60
+ `ream wait`; `ream --help` lists them). The older `artifacts-publish`,
61
+ `artifacts-decide` and the other `artifacts-*` commands keep working for good,
62
+ as do the `ARTIFACTS_*` settings, and `/api/v1/artifacts` answers exactly as
63
+ `/api/v1/pages` does (the same handlers, so a POST to either works).
64
+
65
+ ```bash
66
+ ream publish page.html --title "My page"
67
+ ream publish page.html --update <id> # after editing page.html
68
+ ream publish --list
69
+ ```
70
+
71
+ Where the key comes from, in the order to prefer:
72
+
73
+ ```bash
74
+ # A vault hands it over on a descriptor; no cleartext copy exists. Where keys are
75
+ # vaulted, a wrapper does it (here `artifacts-key`, with your seat's name):
76
+ ARTIFACTS_SEAT=<seat> artifacts-key -- ream publish page.html --title "My page"
77
+
78
+ # Or any store of your own that can open one:
79
+ ream publish --key-fd 3 page.html --title "My page" 3< <(pass show artifacts/key)
80
+
81
+ # Or a file, mode 600 -- it refuses anything looser:
82
+ REAM_KEY_FILE=~/.config/artifacts/key ream publish --list
83
+ ```
84
+
85
+ ## Tidy one up
86
+
87
+ Everything Jeremy can do to a page in the gallery, an agent can do to its own
88
+ pages — his rule, 20 Sep 2026: *"ideally agents can take all the same actions a
89
+ human can."* None of these send the body, so none of them needs the file:
90
+
91
+ ```bash
92
+ ream publish --rename <id> --title "Its real name"
93
+ ream publish --archive <id> # off the wall; nothing is deleted
94
+ ream publish --unarchive <id>
95
+ ream publish --pin <id> # leads the gallery
96
+ ream publish --unpin <id>
97
+ ream publish --duplicate <id> # a copy at a new address
98
+ ```
99
+
100
+ Over HTTP these are `POST /api/v1/pages/{id}/{archive,rename,pin}`, each
101
+ scoped to the calling key's own agent: another seat's page answers 404, and so
102
+ does a page that does not exist — the same 404, so a key cannot be used to
103
+ learn which ids are real.
104
+
105
+ `$REAM_API` (default `https://artifacts.tenfourty.site/api/v1`) and
106
+ `$REAM_KEY_FILE` (default `~/.config/artifacts/key`) point it elsewhere;
107
+ `--key-file` overrides both for one call.
108
+
109
+ ## How deploys happen
110
+
111
+ Every push to `main` runs `.forgejo/workflows/ci.yml`: tests, then an image
112
+ build pushed to the forge's registry as `<repo>:<sha12>`, then a pull request on
113
+ the deployment's own repo that moves its `core.lock` to that image
114
+ (`.forgejo/scripts/core-lock-pr.sh`). That repo builds the deployed image and opens
115
+ the pin PR; production moves when that PR is merged. The runbook is
116
+ `docs/agent/runbooks/image-from-ream-homelab.md`. This repo keeps no deployment
117
+ values: they are in the deployment's own repo.
@@ -0,0 +1,80 @@
1
+ [project]
2
+ name = "ream-artifacts"
3
+ version = "0.1.0"
4
+ description = "A self-hosted artifacts publisher: agents publish HTML pages over a keyed API; Jeremy views them in an Authelia-gated gallery."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = []
8
+
9
+ [[project.authors]]
10
+ name = "Jeremy Brown"
11
+ email = "jeremy@tenfourty.com"
12
+
13
+ [project.optional-dependencies]
14
+ server = [
15
+ "cryptography>=46",
16
+ "fastapi>=0.141.1",
17
+ "html5lib>=1.1",
18
+ "markdown-it-py>=4",
19
+ "markupsafe>=3.0.3",
20
+ "mdit-py-plugins>=0.5",
21
+ "psycopg[binary]>=3.3.6",
22
+ "python-multipart>=0.0.32",
23
+ "uvicorn>=0.53.0",
24
+ "zstandard>=0.25",
25
+ ]
26
+
27
+ [project.scripts]
28
+ ream = "ream.cli:main"
29
+ artifacts-admin = "ream.admin:main"
30
+ artifacts-publish = "ream.client:run"
31
+ artifacts-decide = "ream.decide:run"
32
+ artifacts-republish = "ream.republish:run"
33
+ artifacts-connect = "ream.connect_cli:run"
34
+
35
+ [tool.uv.build-backend]
36
+ module-name = [
37
+ "ream",
38
+ "artifacts",
39
+ ]
40
+
41
+ [tool.pytest.ini_options]
42
+ addopts = "--dist=worksteal -rs"
43
+ timeout = 300
44
+ timeout_method = "signal"
45
+ markers = [
46
+ "slow(reason): genuinely needs more than a second; says why",
47
+ "postgres_only: reads Postgres's catalogue or runs Postgres's own SQL; skipped when REAM_TEST_DB=sqlite",
48
+ "plugin_loader: drives the plug-in loader with entry points it fakes; REAM_PLUGINS is unset for it (the core-bare CI job sets it to none for everything else)",
49
+ "no_default_comments: leave a publish exactly as the test sends it (the suite otherwise adds comments on to a new page's publish, which the service now requires)",
50
+ "background_thumbnails: publishes in this test start the real background thumbnail render (off by default in tests; see tests/conftest.py)",
51
+ ]
52
+
53
+ [tool.ruff.lint]
54
+ extend-select = [
55
+ "S",
56
+ "B",
57
+ ]
58
+
59
+ [tool.ruff.lint.per-file-ignores]
60
+ "tests/**" = [
61
+ "S101",
62
+ "S311",
63
+ "F811",
64
+ ]
65
+
66
+ [build-system]
67
+ requires = ["uv_build>=0.12.9,<0.13.0"]
68
+ build-backend = "uv_build"
69
+
70
+ [dependency-groups]
71
+ dev = [
72
+ "httpx>=0.28.1",
73
+ "pgserver>=0.1.4",
74
+ "playwright>=1.63.0",
75
+ "pytest>=9.1.1",
76
+ "pytest-asyncio>=1.4.0",
77
+ "pytest-timeout>=2.4.0",
78
+ "pytest-xdist>=3.8.0",
79
+ "ruff>=0.16.8",
80
+ ]
@@ -0,0 +1,109 @@
1
+ [project]
2
+ # The install name, which `uv tool install` takes, is `ream-artifacts` (docs/decisions.md, 10 Nov
3
+ # 2026): `ream` on public PyPI is somebody else's package. Nothing else is renamed: the `ream`
4
+ # command, the `artifacts-*` commands, the import packages `ream` and `artifacts`.
5
+ name = "ream-artifacts"
6
+ version = "0.1.0"
7
+ description = "A self-hosted artifacts publisher: agents publish HTML pages over a keyed API; Jeremy views them in an Authelia-gated gallery."
8
+ readme = "README.md"
9
+ authors = [
10
+ { name = "Jeremy Brown", email = "jeremy@tenfourty.com" }
11
+ ]
12
+ requires-python = ">=3.12"
13
+ # NOTHING by default, on purpose (homelab-ij8.68, 23 Sep 2026). The agent tools
14
+ # -- artifacts-publish, -decide, -republish -- use only the standard library, and
15
+ # the service hands this package to agents on other machines from an open
16
+ # address. An agent that wants to publish a page must not have to install a web
17
+ # framework and a database driver to do it. The service itself installs the
18
+ # `server` extra (Dockerfile, CI); for local work: `uv sync --all-extras`.
19
+ dependencies = []
20
+
21
+ [project.optional-dependencies]
22
+ server = [
23
+ "cryptography>=46",
24
+ "fastapi>=0.141.1",
25
+ "html5lib>=1.1",
26
+ "markdown-it-py>=4",
27
+ "markupsafe>=3.0.3",
28
+ "mdit-py-plugins>=0.5",
29
+ "psycopg[binary]>=3.3.6",
30
+ "python-multipart>=0.0.32",
31
+ "uvicorn>=0.53.0",
32
+ "zstandard>=0.25",
33
+ ]
34
+
35
+ [project.scripts]
36
+ ream = "ream.cli:main"
37
+ # The old command names work for good, beside the verbs of `ream`.
38
+ artifacts-admin = "ream.admin:main"
39
+ artifacts-publish = "ream.client:run"
40
+ artifacts-decide = "ream.decide:run"
41
+ artifacts-republish = "ream.republish:run"
42
+ artifacts-connect = "ream.connect_cli:run"
43
+
44
+ # Two top-level packages ship in the one wheel: `ream`, and `artifacts`, the old import
45
+ # name kept for good (a shim whose modules ARE the ream modules).
46
+ [tool.uv.build-backend]
47
+ module-name = ["ream", "artifacts"]
48
+
49
+ [build-system]
50
+ requires = ["uv_build>=0.12.9,<0.13.0"]
51
+ build-backend = "uv_build"
52
+
53
+ [dependency-groups]
54
+ dev = [
55
+ "httpx>=0.28.1",
56
+ "pgserver>=0.1.4",
57
+ "playwright>=1.63.0",
58
+ "pytest>=9.1.1",
59
+ "pytest-asyncio>=1.4.0",
60
+ "pytest-timeout>=2.4.0",
61
+ "pytest-xdist>=3.8.0",
62
+ "ruff>=0.16.8",
63
+ ]
64
+
65
+ [tool.pytest.ini_options]
66
+ # worksteal, not xdist's default `load`: idle workers take tests from the
67
+ # busiest one's tail, so the run ends when the work does, not when the unluckiest
68
+ # worker's last chunk does. Inert without -n. See pytest_collection_modifyitems
69
+ # in tests/conftest.py for where the slow tests go.
70
+ # -rs: the summary names every skipped test and why. Browser tests skip where
71
+ # there is no headless browser (a laptop without one; CI installs chromium and sets
72
+ # REAM_REQUIRE_BROWSER=1, so there a missing browser fails), and a bare "13 skipped"
73
+ # hid that for weeks (runcove-5t3oj.77).
74
+ addopts = "--dist=worksteal -rs"
75
+ # A test that hangs fails after 5 minutes with every thread's stack printed,
76
+ # naming itself, instead of holding a CI builder until the job's own limit
77
+ # (6 Oct 2026: a test job sat idle for 35 minutes and left no clue). The
78
+ # slowest test takes about 9 s; this only ever catches a hang.
79
+ timeout = 300
80
+ # "signal", not "thread": the thread method dumps stacks and kills the worker,
81
+ # and xdist drops a dead worker's stderr -- on 7 Oct 2026 a CI hang left only
82
+ # "node down: Not properly terminated". The signal method fails the hung test
83
+ # in place with its traceback, and the worker carries on.
84
+ timeout_method = "signal"
85
+ # `slow`: a test that genuinely needs more than a second, each with a one-line
86
+ # reason at the mark. At most ten may carry it; everything else stays under 1 s.
87
+ markers = [
88
+ "slow(reason): genuinely needs more than a second; says why",
89
+ "postgres_only: reads Postgres's catalogue or runs Postgres's own SQL; skipped when REAM_TEST_DB=sqlite",
90
+ "plugin_loader: drives the plug-in loader with entry points it fakes; REAM_PLUGINS is unset for it (the core-bare CI job sets it to none for everything else)",
91
+ "no_default_comments: leave a publish exactly as the test sends it (the suite otherwise adds comments on to a new page's publish, which the service now requires)",
92
+ "background_thumbnails: publishes in this test start the real background thumbnail render (off by default in tests; see tests/conftest.py)",
93
+ ]
94
+
95
+ [tool.ruff.lint]
96
+ # S (bandit) and B (bugbear) are worth enabling deliberately on a service whose
97
+ # whole job is handling credentials and publishing agent-authored HTML -- on
98
+ # top of ruff's E4/E7/E9/F + isort defaults, not replacing them.
99
+ extend-select = ["S", "B"]
100
+
101
+ [tool.ruff.lint.per-file-ignores]
102
+ # S101 flags `assert` -- pytest's idiom for every assertion, not a risk here.
103
+ # S311 flags `random.Random` as unfit for cryptographic use -- true, and
104
+ # irrelevant to a seeded fuzz corpus for a property test, which wants
105
+ # reproducible pseudo-randomness, never secrecy.
106
+ # F811 flags a pytest fixture imported from test_client and then taken as a
107
+ # parameter -- how pytest works, not a mistake. Per-line noqa stopped covering
108
+ # it once the formatter wrapped long signatures (homelab-ij8.25).
109
+ "tests/**" = ["S101", "S311", "F811"]
@@ -0,0 +1,91 @@
1
+ """The old import name, kept for good: `import artifacts` is `import ream`.
2
+
3
+ The package was renamed to `ream` (docs/decisions.md, "Ream inside the code too"). Callers
4
+ outside this repository still write `import artifacts`, `from artifacts.settings import ...`,
5
+ `artifacts.plugin_api`, or name `artifacts.client:run` as an entry point, and all of it keeps
6
+ working. Nothing here is a copy:
7
+
8
+ * `artifacts` itself becomes the `ream` module object (so `resources.files("artifacts")` finds
9
+ the schema and the templates in `ream`);
10
+ * `artifacts.X` is the SAME module object as `ream.X`, found by the finder below, so
11
+ `settings.override(...)` or a monkeypatch through either name acts on one module, never on
12
+ two copies that each hold their own state.
13
+
14
+ This file reads no environment and holds no logic of the service.
15
+ """
16
+
17
+ import importlib
18
+ import importlib.abc
19
+ import importlib.util
20
+ import sys
21
+ from importlib.machinery import ModuleSpec
22
+
23
+ import ream
24
+
25
+ _OLD = "artifacts"
26
+ _NEW = "ream"
27
+
28
+
29
+ class _AliasFinder(importlib.abc.MetaPathFinder, importlib.abc.Loader):
30
+ """Answers `artifacts.<x>` with the already-imported `ream.<x>`."""
31
+
32
+ def __init__(self) -> None:
33
+ self._real_specs: dict[str, ModuleSpec] = {}
34
+
35
+ def find_spec(self, fullname, path=None, target=None) -> ModuleSpec | None:
36
+ if not fullname.startswith(_OLD + "."):
37
+ return None
38
+ real = _NEW + fullname[len(_OLD) :]
39
+ try:
40
+ found = importlib.util.find_spec(real)
41
+ except ModuleNotFoundError:
42
+ return None
43
+ if found is None:
44
+ return None
45
+ self._real_spec(fullname)
46
+ return importlib.util.spec_from_loader(fullname, self)
47
+
48
+ def create_module(self, spec: ModuleSpec):
49
+ return importlib.import_module(_NEW + spec.name[len(_OLD) :])
50
+
51
+ def _real_spec(self, fullname: str) -> ModuleSpec | None:
52
+ """The spec `ream.<x>` was really imported with. Once `artifacts.<x>` has been
53
+ imported the import system overwrites `module.__spec__` with the alias spec, so
54
+ the first real one seen is kept."""
55
+ real = _NEW + fullname[len(_OLD) :]
56
+ if real not in self._real_specs:
57
+ found = importlib.util.find_spec(real)
58
+ if found is None or found.name != real:
59
+ return found
60
+ self._real_specs[real] = found
61
+ return self._real_specs[real]
62
+
63
+ def exec_module(self, module) -> None:
64
+ """Nothing to run: the module was executed under its real name. The import
65
+ system has just set `module.__spec__` to the alias spec; put the real one back."""
66
+ real = self._real_spec(_OLD + module.__name__[len(_NEW) :])
67
+ if real is not None:
68
+ module.__spec__ = real
69
+
70
+ def _real_loader(self, fullname: str):
71
+ real = self._real_spec(fullname)
72
+ if real is None or real.loader is None:
73
+ raise ImportError(f"no loader for {fullname}", name=fullname)
74
+ return real.loader
75
+
76
+ # `python -m artifacts.<x>` (runpy) asks the alias spec's loader for the code.
77
+ def get_code(self, fullname: str):
78
+ return self._real_loader(fullname).get_code(_NEW + fullname[len(_OLD) :])
79
+
80
+ def get_source(self, fullname: str):
81
+ return self._real_loader(fullname).get_source(_NEW + fullname[len(_OLD) :])
82
+
83
+ def is_package(self, fullname: str) -> bool:
84
+ return self._real_loader(fullname).is_package(_NEW + fullname[len(_OLD) :])
85
+
86
+
87
+ if not any(isinstance(f, _AliasFinder) for f in sys.meta_path):
88
+ sys.meta_path.insert(0, _AliasFinder())
89
+
90
+ # `import artifacts` hands back the ream package itself.
91
+ sys.modules[__name__] = ream
File without changes
File without changes