splent-feature-spl-registry 0.1.2__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 (32) hide show
  1. splent_feature_spl_registry-0.1.2/MANIFEST.in +24 -0
  2. splent_feature_spl_registry-0.1.2/PKG-INFO +179 -0
  3. splent_feature_spl_registry-0.1.2/README.md +172 -0
  4. splent_feature_spl_registry-0.1.2/pyproject.toml +87 -0
  5. splent_feature_spl_registry-0.1.2/setup.cfg +4 -0
  6. splent_feature_spl_registry-0.1.2/src/splent_feature_spl_registry.egg-info/PKG-INFO +179 -0
  7. splent_feature_spl_registry-0.1.2/src/splent_feature_spl_registry.egg-info/SOURCES.txt +30 -0
  8. splent_feature_spl_registry-0.1.2/src/splent_feature_spl_registry.egg-info/dependency_links.txt +1 -0
  9. splent_feature_spl_registry-0.1.2/src/splent_feature_spl_registry.egg-info/requires.txt +1 -0
  10. splent_feature_spl_registry-0.1.2/src/splent_feature_spl_registry.egg-info/top_level.txt +1 -0
  11. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/__init__.py +25 -0
  12. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/commands.py +396 -0
  13. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/config.py +100 -0
  14. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/gate.py +138 -0
  15. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/i18n.py +34 -0
  16. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/migrations/alembic.ini +36 -0
  17. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/migrations/env.py +9 -0
  18. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/migrations/script.py.mako +24 -0
  19. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/migrations/versions/67b0eb0c4e87_splent_feature_spl_registry.py +102 -0
  20. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/migrations/versions/a1c7f3e90b12_add_listed_to_spl.py +40 -0
  21. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/migrations/versions/c4d81a5f2e30_add_spl_key_call.py +58 -0
  22. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/models.py +374 -0
  23. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/repositories.py +204 -0
  24. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/routes.py +149 -0
  25. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/seeders.py +173 -0
  26. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/services.py +1204 -0
  27. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/templates/spl_registry/detail.html +126 -0
  28. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/templates/spl_registry/not_found.html +15 -0
  29. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/translations/es/LC_MESSAGES/messages.mo +0 -0
  30. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/translations/es/LC_MESSAGES/messages.po +379 -0
  31. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/uvl.py +417 -0
  32. splent_feature_spl_registry-0.1.2/src/splent_io/splent_feature_spl_registry/uvlhub.py +257 -0
@@ -0,0 +1,24 @@
1
+ # Frontend assets: the whole tree, not only the compiled bundles.
2
+ #
3
+ # A feature that registers a plain stylesheet with register_asset("css", …)
4
+ # serves it straight from assets/css, and a vendored library lives under
5
+ # assets/vendor. Shipping assets/dist alone put no stylesheet at all in the
6
+ # built package, so every production image came up without its theme, its
7
+ # skin and its editor, while development looked fine because that path
8
+ # installs the checkout in place and reads the files off disk.
9
+ recursive-include src/splent_io/splent_feature_spl_registry/assets *.css *.js *.map *.json
10
+ recursive-include src/splent_io/splent_feature_spl_registry/assets *.svg *.png *.jpg *.jpeg *.gif *.webp *.ico
11
+ recursive-include src/splent_io/splent_feature_spl_registry/assets *.woff *.woff2 *.ttf *.eot
12
+
13
+ # Build inputs, never shipped.
14
+ prune src/splent_io/splent_feature_spl_registry/assets/node_modules
15
+
16
+ # Jinja templates (feature views + hook fragments)
17
+ recursive-include src/splent_io/splent_feature_spl_registry/templates *.html
18
+
19
+ # Alembic migration config and scripts
20
+ recursive-include src/splent_io/splent_feature_spl_registry/migrations *.py *.ini *.mako
21
+
22
+ # Translations. The .mo is what gettext reads at runtime; the .po
23
+ # travels with it so the next translator has something to edit.
24
+ recursive-include src/splent_io/splent_feature_spl_registry/translations *.mo *.po *.pot
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.4
2
+ Name: splent_feature_spl_registry
3
+ Version: 0.1.2
4
+ Requires-Python: >=3.13
5
+ Description-Content-Type: text/markdown
6
+ Requires-Dist: requests>=2.31
7
+
8
+ # splent_feature_spl_registry
9
+
10
+ The marketplace's registry of software product lines. It owns which SPLs exist,
11
+ who owns each one, the DOI pointer to the published UVL model, and the relay
12
+ that publishes a model to UVLHub.
13
+
14
+ It subsumes `splent_catalog`, the git repository that used to hold a
15
+ `metadata.toml` per product line. Everything those files carried (name,
16
+ description and the `{mirror, doi, file}` pointer) now lives in the `spl`
17
+ table, and `GET /api/v1/spls` serves it to the splent CLI and the index
18
+ builder.
19
+
20
+ ## The model
21
+
22
+ ```
23
+ developer --(splent CLI with a splent token)--> MARKETPLACE --(one server-side key)--> UVLHub
24
+ ```
25
+
26
+ A developer authenticates to the marketplace with a token from
27
+ `splent_feature_apikeys` and sends a `.uvl` file. They never hold UVLHub
28
+ credentials, never name a UVLHub dataset and never learn UVLHub is involved.
29
+ The marketplace validates the model, relays it with its single key, and stores
30
+ the DOI pointer. The UVL content itself is never stored here.
31
+
32
+ Publishing a FEATURE is unchanged: `splent feature:release`, no account, no
33
+ token, the git tag is the publication. This feature is only about product
34
+ lines.
35
+
36
+ ## Ordering
37
+
38
+ A release row is written **before** the irreversible call to UVLHub, then
39
+ updated with the outcome. This is the point of the design. If the publish call
40
+ times out and nothing was written down first, the marketplace cannot tell
41
+ whether a DOI was minted, and the honest answer ("I do not know") gets replaced
42
+ by the tempting one ("try again"), which mints a second permanent DOI for the
43
+ same model.
44
+
45
+ A release therefore settles into one of three states, and only one of them is
46
+ safe to retry:
47
+
48
+ | state | meaning | retry |
49
+ |-------------|--------------------------------------------|-------|
50
+ | `published` | UVLHub minted a DOI, recorded here | not needed, republishing the same bytes is idempotent |
51
+ | `failed` | UVLHub refused. Nothing was minted | yes |
52
+ | `unknown` | No answer after the irreversible call | **no**, an administrator settles it first |
53
+
54
+ `splent feature:spl_registry stuck` lists the undetermined ones and
55
+ `... settle <spl> <version> [--doi ...]` records what was found.
56
+
57
+ ## Endpoints
58
+
59
+ | method | path | auth |
60
+ |--------|---------------------------------|--------------------|
61
+ | GET | `/api/v1/spls` | public |
62
+ | GET | `/api/v1/spls/<name>` | public |
63
+ | POST | `/api/v1/spls/<name>/releases` | Bearer, `write_spl` |
64
+ | GET | `/marketplace/spls/<name>` | public page |
65
+
66
+ ```bash
67
+ curl -X POST https://<marketplace>/api/v1/spls/my_line/releases \
68
+ -H "Authorization: Bearer splent_..." \
69
+ -F "file=@my_line.uvl"
70
+ ```
71
+
72
+ ## Configuration
73
+
74
+ Every variable has a working default; the feature runs with no `.env` entry.
75
+
76
+ | variable | default | what it does |
77
+ |--------------------------------------|--------------------------|--------------|
78
+ | `UVLHUB_API_KEY` | (none) | the single server-side key. Without it the write path answers 503 |
79
+ | `UVLHUB_URL` | `https://www.uvlhub.io` | which instance to relay to |
80
+ | `SPL_REGISTRY_PUBLISHING_ENABLED` | `true` | kill switch. `false` closes publishing and leaves every read path up |
81
+ | `SPL_REGISTRY_VERIFY_PUBLISHED` | `true` | download the published file and compare it byte for byte |
82
+ | `SPL_REGISTRY_UVLHUB_TIMEOUT` | `60` | seconds per UVLHub call |
83
+ | `SPL_REGISTRY_QUOTA_WINDOW_HOURS` | `24` | the rolling window every quota is counted over |
84
+ | `SPL_REGISTRY_MAX_RELEASES_PER_USER` | `10` | per developer, per window |
85
+ | `SPL_REGISTRY_MAX_RELEASES_PER_SPL` | `5` | per product line, per window |
86
+ | `SPL_REGISTRY_MAX_RELEASES_TOTAL` | `100` | the ceiling that bounds the single key |
87
+ | `SPL_REGISTRY_MAX_SPLS_PER_USER` | `5` | product lines one developer may own |
88
+
89
+ `UVLHUB_API_KEY` is deliberately never copied into `app.config`: that mapping
90
+ is dumped by `splent product:config`, by the settings screens and by Flask's
91
+ debug pages. It is read from the environment at call time in exactly one place,
92
+ `uvlhub.api_key()`, which is the only place in the whole workspace that reads
93
+ it server side.
94
+
95
+ Quotas are counted with SQL `COUNT`s, never in-process counters, because
96
+ several workers run in parallel and a per-process counter would multiply every
97
+ allowance by the number of them.
98
+
99
+ ## 403 or 404
100
+
101
+ Every product line is in the public index unless an administrator withdraws it
102
+ with `visibility <spl> withdrawn`. That one flag decides what the publishing
103
+ endpoint says about a line that is not yours:
104
+
105
+ | the line is | answer | why |
106
+ |--------------|--------|-----|
107
+ | listed | `403` | admitting the name is taken tells the caller nothing they cannot read in `GET /api/v1/spls` |
108
+ | withdrawn | `404` | the same answer, code and wording as a name nobody registered, so the write path cannot be walked to find what the read path hides |
109
+
110
+ Withdrawing hides a line. It deletes nothing: every release and every DOI stays
111
+ exactly where it is, and the owner can still publish to it.
112
+
113
+ ## Authorship
114
+
115
+ A publication is credited to the publishing developer's profile, read server
116
+ side and never from a request field, because an author line a stranger can
117
+ choose is an impersonation vector on a permanent record.
118
+
119
+ The profile feature is **not** required. When the product installs it, the
120
+ credit is the `name` and `surname` on the developer's `UserProfile`, read
121
+ through `UserProfileService.get_by_user_id`. When it does not, or when the
122
+ developer never filled the form in, the credit is `SPLENT MARKETPLACE`. It is
123
+ never the email address, which would put a personal address on a permanent
124
+ public record.
125
+
126
+ A SPLENT profile holds a name and a surname and nothing else, so that is all
127
+ that is sent. There is no affiliation and no ORCID to send.
128
+
129
+ ## Commands
130
+
131
+ ```bash
132
+ splent db:seed
133
+ splent feature:spl_registry import-catalog [--path ...] [--owner email] [--dry-run]
134
+ splent feature:spl_registry list
135
+ splent feature:spl_registry stuck [--hours 1]
136
+ splent feature:spl_registry settle <spl> <version> [--doi ...] [--note ...]
137
+ splent feature:spl_registry set-owner <spl> <email>
138
+ splent feature:spl_registry visibility <spl> public|withdrawn
139
+ ```
140
+
141
+ `splent db:seed` runs `SplRegistrySeeder`, which **carries the three
142
+ `splent_catalog` lines as data**: their name, description, mirror, DOI and file
143
+ name are written out in `seeders.py`. That is the copy that survives the
144
+ directory being deleted, and it is what rebuilds the registry on a fresh
145
+ database. `import-catalog` does the same job by *reading* the directory, so use
146
+ it for the one-off migration while `splent_catalog` is still on disk and the
147
+ seeder from then on. Both are idempotent.
148
+
149
+ Neither records a content digest for the lines it brings in. The digest is the
150
+ idempotency key, and a release carrying one is the registry promising these
151
+ exact bytes are what that DOI serves. Nothing has compared them, and one of the
152
+ three does not even point at the file sitting next to it
153
+ (`sample_splent_spl` serves `sample_splent_app.uvl`), so the digest is left
154
+ empty and the local one is recorded as a note instead.
155
+
156
+ Imported lines arrive with **no owner**, which is what stops any authenticated
157
+ developer from publishing to them; `set-owner` hands one over.
158
+
159
+ ## Requires
160
+
161
+ `auth` (the User an owner and a publisher point at) and `apikeys` (the
162
+ `write_spl` Bearer token the publishing endpoint is guarded by).
163
+
164
+ `profile` is **optional**. It improves the credit on a publication and the name
165
+ shown on the public page, and the feature works without it.
166
+
167
+ ## Tests
168
+
169
+ ```bash
170
+ splent feature:test splent_feature_spl_registry
171
+ ```
172
+
173
+ Unit, integration and functional. The suite never reaches UVLHub: the conftest
174
+ replaces the four relay calls **and** the HTTP client underneath them, and it
175
+ does so through its own `MonkeyPatch` rather than the shared fixture. That last
176
+ detail is not decoration. While the shared fixture was used, a single
177
+ `monkeypatch.undo()` inside a test restored the real relay along with the real
178
+ `UVLHUB_API_KEY`, and a test run minted a permanent DOI on production Zenodo.
179
+ Publishing is irreversible, so the guard has to be one a test cannot lift.
@@ -0,0 +1,172 @@
1
+ # splent_feature_spl_registry
2
+
3
+ The marketplace's registry of software product lines. It owns which SPLs exist,
4
+ who owns each one, the DOI pointer to the published UVL model, and the relay
5
+ that publishes a model to UVLHub.
6
+
7
+ It subsumes `splent_catalog`, the git repository that used to hold a
8
+ `metadata.toml` per product line. Everything those files carried (name,
9
+ description and the `{mirror, doi, file}` pointer) now lives in the `spl`
10
+ table, and `GET /api/v1/spls` serves it to the splent CLI and the index
11
+ builder.
12
+
13
+ ## The model
14
+
15
+ ```
16
+ developer --(splent CLI with a splent token)--> MARKETPLACE --(one server-side key)--> UVLHub
17
+ ```
18
+
19
+ A developer authenticates to the marketplace with a token from
20
+ `splent_feature_apikeys` and sends a `.uvl` file. They never hold UVLHub
21
+ credentials, never name a UVLHub dataset and never learn UVLHub is involved.
22
+ The marketplace validates the model, relays it with its single key, and stores
23
+ the DOI pointer. The UVL content itself is never stored here.
24
+
25
+ Publishing a FEATURE is unchanged: `splent feature:release`, no account, no
26
+ token, the git tag is the publication. This feature is only about product
27
+ lines.
28
+
29
+ ## Ordering
30
+
31
+ A release row is written **before** the irreversible call to UVLHub, then
32
+ updated with the outcome. This is the point of the design. If the publish call
33
+ times out and nothing was written down first, the marketplace cannot tell
34
+ whether a DOI was minted, and the honest answer ("I do not know") gets replaced
35
+ by the tempting one ("try again"), which mints a second permanent DOI for the
36
+ same model.
37
+
38
+ A release therefore settles into one of three states, and only one of them is
39
+ safe to retry:
40
+
41
+ | state | meaning | retry |
42
+ |-------------|--------------------------------------------|-------|
43
+ | `published` | UVLHub minted a DOI, recorded here | not needed, republishing the same bytes is idempotent |
44
+ | `failed` | UVLHub refused. Nothing was minted | yes |
45
+ | `unknown` | No answer after the irreversible call | **no**, an administrator settles it first |
46
+
47
+ `splent feature:spl_registry stuck` lists the undetermined ones and
48
+ `... settle <spl> <version> [--doi ...]` records what was found.
49
+
50
+ ## Endpoints
51
+
52
+ | method | path | auth |
53
+ |--------|---------------------------------|--------------------|
54
+ | GET | `/api/v1/spls` | public |
55
+ | GET | `/api/v1/spls/<name>` | public |
56
+ | POST | `/api/v1/spls/<name>/releases` | Bearer, `write_spl` |
57
+ | GET | `/marketplace/spls/<name>` | public page |
58
+
59
+ ```bash
60
+ curl -X POST https://<marketplace>/api/v1/spls/my_line/releases \
61
+ -H "Authorization: Bearer splent_..." \
62
+ -F "file=@my_line.uvl"
63
+ ```
64
+
65
+ ## Configuration
66
+
67
+ Every variable has a working default; the feature runs with no `.env` entry.
68
+
69
+ | variable | default | what it does |
70
+ |--------------------------------------|--------------------------|--------------|
71
+ | `UVLHUB_API_KEY` | (none) | the single server-side key. Without it the write path answers 503 |
72
+ | `UVLHUB_URL` | `https://www.uvlhub.io` | which instance to relay to |
73
+ | `SPL_REGISTRY_PUBLISHING_ENABLED` | `true` | kill switch. `false` closes publishing and leaves every read path up |
74
+ | `SPL_REGISTRY_VERIFY_PUBLISHED` | `true` | download the published file and compare it byte for byte |
75
+ | `SPL_REGISTRY_UVLHUB_TIMEOUT` | `60` | seconds per UVLHub call |
76
+ | `SPL_REGISTRY_QUOTA_WINDOW_HOURS` | `24` | the rolling window every quota is counted over |
77
+ | `SPL_REGISTRY_MAX_RELEASES_PER_USER` | `10` | per developer, per window |
78
+ | `SPL_REGISTRY_MAX_RELEASES_PER_SPL` | `5` | per product line, per window |
79
+ | `SPL_REGISTRY_MAX_RELEASES_TOTAL` | `100` | the ceiling that bounds the single key |
80
+ | `SPL_REGISTRY_MAX_SPLS_PER_USER` | `5` | product lines one developer may own |
81
+
82
+ `UVLHUB_API_KEY` is deliberately never copied into `app.config`: that mapping
83
+ is dumped by `splent product:config`, by the settings screens and by Flask's
84
+ debug pages. It is read from the environment at call time in exactly one place,
85
+ `uvlhub.api_key()`, which is the only place in the whole workspace that reads
86
+ it server side.
87
+
88
+ Quotas are counted with SQL `COUNT`s, never in-process counters, because
89
+ several workers run in parallel and a per-process counter would multiply every
90
+ allowance by the number of them.
91
+
92
+ ## 403 or 404
93
+
94
+ Every product line is in the public index unless an administrator withdraws it
95
+ with `visibility <spl> withdrawn`. That one flag decides what the publishing
96
+ endpoint says about a line that is not yours:
97
+
98
+ | the line is | answer | why |
99
+ |--------------|--------|-----|
100
+ | listed | `403` | admitting the name is taken tells the caller nothing they cannot read in `GET /api/v1/spls` |
101
+ | withdrawn | `404` | the same answer, code and wording as a name nobody registered, so the write path cannot be walked to find what the read path hides |
102
+
103
+ Withdrawing hides a line. It deletes nothing: every release and every DOI stays
104
+ exactly where it is, and the owner can still publish to it.
105
+
106
+ ## Authorship
107
+
108
+ A publication is credited to the publishing developer's profile, read server
109
+ side and never from a request field, because an author line a stranger can
110
+ choose is an impersonation vector on a permanent record.
111
+
112
+ The profile feature is **not** required. When the product installs it, the
113
+ credit is the `name` and `surname` on the developer's `UserProfile`, read
114
+ through `UserProfileService.get_by_user_id`. When it does not, or when the
115
+ developer never filled the form in, the credit is `SPLENT MARKETPLACE`. It is
116
+ never the email address, which would put a personal address on a permanent
117
+ public record.
118
+
119
+ A SPLENT profile holds a name and a surname and nothing else, so that is all
120
+ that is sent. There is no affiliation and no ORCID to send.
121
+
122
+ ## Commands
123
+
124
+ ```bash
125
+ splent db:seed
126
+ splent feature:spl_registry import-catalog [--path ...] [--owner email] [--dry-run]
127
+ splent feature:spl_registry list
128
+ splent feature:spl_registry stuck [--hours 1]
129
+ splent feature:spl_registry settle <spl> <version> [--doi ...] [--note ...]
130
+ splent feature:spl_registry set-owner <spl> <email>
131
+ splent feature:spl_registry visibility <spl> public|withdrawn
132
+ ```
133
+
134
+ `splent db:seed` runs `SplRegistrySeeder`, which **carries the three
135
+ `splent_catalog` lines as data**: their name, description, mirror, DOI and file
136
+ name are written out in `seeders.py`. That is the copy that survives the
137
+ directory being deleted, and it is what rebuilds the registry on a fresh
138
+ database. `import-catalog` does the same job by *reading* the directory, so use
139
+ it for the one-off migration while `splent_catalog` is still on disk and the
140
+ seeder from then on. Both are idempotent.
141
+
142
+ Neither records a content digest for the lines it brings in. The digest is the
143
+ idempotency key, and a release carrying one is the registry promising these
144
+ exact bytes are what that DOI serves. Nothing has compared them, and one of the
145
+ three does not even point at the file sitting next to it
146
+ (`sample_splent_spl` serves `sample_splent_app.uvl`), so the digest is left
147
+ empty and the local one is recorded as a note instead.
148
+
149
+ Imported lines arrive with **no owner**, which is what stops any authenticated
150
+ developer from publishing to them; `set-owner` hands one over.
151
+
152
+ ## Requires
153
+
154
+ `auth` (the User an owner and a publisher point at) and `apikeys` (the
155
+ `write_spl` Bearer token the publishing endpoint is guarded by).
156
+
157
+ `profile` is **optional**. It improves the credit on a publication and the name
158
+ shown on the public page, and the feature works without it.
159
+
160
+ ## Tests
161
+
162
+ ```bash
163
+ splent feature:test splent_feature_spl_registry
164
+ ```
165
+
166
+ Unit, integration and functional. The suite never reaches UVLHub: the conftest
167
+ replaces the four relay calls **and** the HTTP client underneath them, and it
168
+ does so through its own `MonkeyPatch` rather than the shared fixture. That last
169
+ detail is not decoration. While the shared fixture was used, a single
170
+ `monkeypatch.undo()` inside a test restored the real relay along with the real
171
+ `UVLHUB_API_KEY`, and a test run minted a permanent DOI on production Zenodo.
172
+ Publishing is irreversible, so the guard has to be one a test cannot lift.
@@ -0,0 +1,87 @@
1
+ [build-system]
2
+ requires = ["setuptools>=80.3.1", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "splent_feature_spl_registry"
7
+ version = "0.1.2"
8
+ readme = "README.md"
9
+ requires-python = ">=3.13"
10
+
11
+ dependencies = [
12
+ # The relay to UVLHub. Declared even though the runtime image already
13
+ # carries it, because this feature does not work without it.
14
+ "requests>=2.31",
15
+ ]
16
+
17
+ [tool.setuptools]
18
+ package-dir = { "" = "src" }
19
+ include-package-data = true
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["src"]
23
+ exclude = ["*.tests", "*.tests.*"]
24
+
25
+ [tool.splent]
26
+ cli_version = "1.14.0"
27
+
28
+ # ── Feature Contract (auto-generated) ────────────────────────────────────────
29
+ # Do not edit manually — re-run `splent feature:contract --write` to refresh.
30
+ # Preserved across refreshes: description, category, tags, env,
31
+ # requires.features_manual (dependencies invisible to static analysis)
32
+ # and provides.routes_manual (routes built from configuration at
33
+ # registration time, which have no literal path to find in source).
34
+ [tool.splent.contract]
35
+ description = "Registry of software product lines, and the relay that publishes their UVL models to UVLHub"
36
+ archetype = "full"
37
+
38
+ [tool.splent.contract.provides]
39
+ routes = ["/api/v1/spls", "/api/v1/spls/<name>", "/api/v1/spls/<name>/releases", "/marketplace/spls/<name>"]
40
+ blueprints = []
41
+ models = ["Spl", "SplKeyCall", "SplRelease"]
42
+ commands = ["import-catalog", "list", "set-owner", "settle", "stuck", "visibility"]
43
+ hooks = []
44
+ services = ["SplRegistryService"]
45
+ signals = []
46
+ translations = ["es"]
47
+ docker = []
48
+
49
+ [tool.splent.contract.requires]
50
+ features = ["apikeys", "auth"]
51
+ env_vars = ["SPLENT_CATALOG_PATH", "SPL_REGISTRY_GATE_TIMEOUT_SECONDS", "SPL_REGISTRY_MAX_RELEASES_PER_SPL", "SPL_REGISTRY_MAX_RELEASES_PER_USER", "SPL_REGISTRY_MAX_RELEASES_TOTAL", "SPL_REGISTRY_MAX_SPLS_PER_USER", "SPL_REGISTRY_PUBLISHING_ENABLED", "SPL_REGISTRY_QUOTA_WINDOW_HOURS", "SPL_REGISTRY_UVLHUB_TIMEOUT", "SPL_REGISTRY_VERIFY_PUBLISHED", "UVLHUB_API_KEY", "UVLHUB_URL"]
52
+ signals = []
53
+
54
+ [tool.splent.contract.extensible]
55
+ services = ["SplRegistryService"]
56
+ templates = ["spl_registry/detail.html", "spl_registry/not_found.html"]
57
+ models = ["Spl", "SplKeyCall", "SplRelease"]
58
+ hooks = []
59
+ routes = false
60
+
61
+ # ── Configuration ─────────────────────────────────────────────────────────
62
+ [tool.splent.config]
63
+ UVLHUB_URL = "https://www.uvlhub.io"
64
+ # The instance's publishing credential, so the default is empty. The real
65
+ # value belongs in the deployment's .env, which is not committed. The code
66
+ # reads it from the environment at call time and never copies it into
67
+ # app.config, so config dumps and debug pages cannot leak it.
68
+ UVLHUB_API_KEY = ""
69
+ # Publishing is off until a deployment turns it on, so an instance brought
70
+ # up to look around cannot push to a public registry by accident.
71
+ SPL_REGISTRY_PUBLISHING_ENABLED = "false"
72
+ # Whether a release is read back after publishing to confirm it landed.
73
+ SPL_REGISTRY_VERIFY_PUBLISHED = "true"
74
+ # Timeouts, in seconds: the gate check is meant to be quick, the upload is
75
+ # not, because a model with its files takes a while.
76
+ SPL_REGISTRY_GATE_TIMEOUT_SECONDS = 10
77
+ SPL_REGISTRY_UVLHUB_TIMEOUT = 60
78
+ # Quotas. The window they are counted over, then the ceilings: per user, per
79
+ # SPL, and across the instance, so one account cannot spend a shared
80
+ # registry's budget.
81
+ SPL_REGISTRY_QUOTA_WINDOW_HOURS = 24
82
+ SPL_REGISTRY_MAX_RELEASES_PER_USER = 10
83
+ SPL_REGISTRY_MAX_RELEASES_PER_SPL = 5
84
+ SPL_REGISTRY_MAX_RELEASES_TOTAL = 100
85
+ SPL_REGISTRY_MAX_SPLS_PER_USER = 5
86
+ # Where the catalog of SPL definitions is read from.
87
+ SPLENT_CATALOG_PATH = "/workspace/splent_catalog"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.4
2
+ Name: splent_feature_spl_registry
3
+ Version: 0.1.2
4
+ Requires-Python: >=3.13
5
+ Description-Content-Type: text/markdown
6
+ Requires-Dist: requests>=2.31
7
+
8
+ # splent_feature_spl_registry
9
+
10
+ The marketplace's registry of software product lines. It owns which SPLs exist,
11
+ who owns each one, the DOI pointer to the published UVL model, and the relay
12
+ that publishes a model to UVLHub.
13
+
14
+ It subsumes `splent_catalog`, the git repository that used to hold a
15
+ `metadata.toml` per product line. Everything those files carried (name,
16
+ description and the `{mirror, doi, file}` pointer) now lives in the `spl`
17
+ table, and `GET /api/v1/spls` serves it to the splent CLI and the index
18
+ builder.
19
+
20
+ ## The model
21
+
22
+ ```
23
+ developer --(splent CLI with a splent token)--> MARKETPLACE --(one server-side key)--> UVLHub
24
+ ```
25
+
26
+ A developer authenticates to the marketplace with a token from
27
+ `splent_feature_apikeys` and sends a `.uvl` file. They never hold UVLHub
28
+ credentials, never name a UVLHub dataset and never learn UVLHub is involved.
29
+ The marketplace validates the model, relays it with its single key, and stores
30
+ the DOI pointer. The UVL content itself is never stored here.
31
+
32
+ Publishing a FEATURE is unchanged: `splent feature:release`, no account, no
33
+ token, the git tag is the publication. This feature is only about product
34
+ lines.
35
+
36
+ ## Ordering
37
+
38
+ A release row is written **before** the irreversible call to UVLHub, then
39
+ updated with the outcome. This is the point of the design. If the publish call
40
+ times out and nothing was written down first, the marketplace cannot tell
41
+ whether a DOI was minted, and the honest answer ("I do not know") gets replaced
42
+ by the tempting one ("try again"), which mints a second permanent DOI for the
43
+ same model.
44
+
45
+ A release therefore settles into one of three states, and only one of them is
46
+ safe to retry:
47
+
48
+ | state | meaning | retry |
49
+ |-------------|--------------------------------------------|-------|
50
+ | `published` | UVLHub minted a DOI, recorded here | not needed, republishing the same bytes is idempotent |
51
+ | `failed` | UVLHub refused. Nothing was minted | yes |
52
+ | `unknown` | No answer after the irreversible call | **no**, an administrator settles it first |
53
+
54
+ `splent feature:spl_registry stuck` lists the undetermined ones and
55
+ `... settle <spl> <version> [--doi ...]` records what was found.
56
+
57
+ ## Endpoints
58
+
59
+ | method | path | auth |
60
+ |--------|---------------------------------|--------------------|
61
+ | GET | `/api/v1/spls` | public |
62
+ | GET | `/api/v1/spls/<name>` | public |
63
+ | POST | `/api/v1/spls/<name>/releases` | Bearer, `write_spl` |
64
+ | GET | `/marketplace/spls/<name>` | public page |
65
+
66
+ ```bash
67
+ curl -X POST https://<marketplace>/api/v1/spls/my_line/releases \
68
+ -H "Authorization: Bearer splent_..." \
69
+ -F "file=@my_line.uvl"
70
+ ```
71
+
72
+ ## Configuration
73
+
74
+ Every variable has a working default; the feature runs with no `.env` entry.
75
+
76
+ | variable | default | what it does |
77
+ |--------------------------------------|--------------------------|--------------|
78
+ | `UVLHUB_API_KEY` | (none) | the single server-side key. Without it the write path answers 503 |
79
+ | `UVLHUB_URL` | `https://www.uvlhub.io` | which instance to relay to |
80
+ | `SPL_REGISTRY_PUBLISHING_ENABLED` | `true` | kill switch. `false` closes publishing and leaves every read path up |
81
+ | `SPL_REGISTRY_VERIFY_PUBLISHED` | `true` | download the published file and compare it byte for byte |
82
+ | `SPL_REGISTRY_UVLHUB_TIMEOUT` | `60` | seconds per UVLHub call |
83
+ | `SPL_REGISTRY_QUOTA_WINDOW_HOURS` | `24` | the rolling window every quota is counted over |
84
+ | `SPL_REGISTRY_MAX_RELEASES_PER_USER` | `10` | per developer, per window |
85
+ | `SPL_REGISTRY_MAX_RELEASES_PER_SPL` | `5` | per product line, per window |
86
+ | `SPL_REGISTRY_MAX_RELEASES_TOTAL` | `100` | the ceiling that bounds the single key |
87
+ | `SPL_REGISTRY_MAX_SPLS_PER_USER` | `5` | product lines one developer may own |
88
+
89
+ `UVLHUB_API_KEY` is deliberately never copied into `app.config`: that mapping
90
+ is dumped by `splent product:config`, by the settings screens and by Flask's
91
+ debug pages. It is read from the environment at call time in exactly one place,
92
+ `uvlhub.api_key()`, which is the only place in the whole workspace that reads
93
+ it server side.
94
+
95
+ Quotas are counted with SQL `COUNT`s, never in-process counters, because
96
+ several workers run in parallel and a per-process counter would multiply every
97
+ allowance by the number of them.
98
+
99
+ ## 403 or 404
100
+
101
+ Every product line is in the public index unless an administrator withdraws it
102
+ with `visibility <spl> withdrawn`. That one flag decides what the publishing
103
+ endpoint says about a line that is not yours:
104
+
105
+ | the line is | answer | why |
106
+ |--------------|--------|-----|
107
+ | listed | `403` | admitting the name is taken tells the caller nothing they cannot read in `GET /api/v1/spls` |
108
+ | withdrawn | `404` | the same answer, code and wording as a name nobody registered, so the write path cannot be walked to find what the read path hides |
109
+
110
+ Withdrawing hides a line. It deletes nothing: every release and every DOI stays
111
+ exactly where it is, and the owner can still publish to it.
112
+
113
+ ## Authorship
114
+
115
+ A publication is credited to the publishing developer's profile, read server
116
+ side and never from a request field, because an author line a stranger can
117
+ choose is an impersonation vector on a permanent record.
118
+
119
+ The profile feature is **not** required. When the product installs it, the
120
+ credit is the `name` and `surname` on the developer's `UserProfile`, read
121
+ through `UserProfileService.get_by_user_id`. When it does not, or when the
122
+ developer never filled the form in, the credit is `SPLENT MARKETPLACE`. It is
123
+ never the email address, which would put a personal address on a permanent
124
+ public record.
125
+
126
+ A SPLENT profile holds a name and a surname and nothing else, so that is all
127
+ that is sent. There is no affiliation and no ORCID to send.
128
+
129
+ ## Commands
130
+
131
+ ```bash
132
+ splent db:seed
133
+ splent feature:spl_registry import-catalog [--path ...] [--owner email] [--dry-run]
134
+ splent feature:spl_registry list
135
+ splent feature:spl_registry stuck [--hours 1]
136
+ splent feature:spl_registry settle <spl> <version> [--doi ...] [--note ...]
137
+ splent feature:spl_registry set-owner <spl> <email>
138
+ splent feature:spl_registry visibility <spl> public|withdrawn
139
+ ```
140
+
141
+ `splent db:seed` runs `SplRegistrySeeder`, which **carries the three
142
+ `splent_catalog` lines as data**: their name, description, mirror, DOI and file
143
+ name are written out in `seeders.py`. That is the copy that survives the
144
+ directory being deleted, and it is what rebuilds the registry on a fresh
145
+ database. `import-catalog` does the same job by *reading* the directory, so use
146
+ it for the one-off migration while `splent_catalog` is still on disk and the
147
+ seeder from then on. Both are idempotent.
148
+
149
+ Neither records a content digest for the lines it brings in. The digest is the
150
+ idempotency key, and a release carrying one is the registry promising these
151
+ exact bytes are what that DOI serves. Nothing has compared them, and one of the
152
+ three does not even point at the file sitting next to it
153
+ (`sample_splent_spl` serves `sample_splent_app.uvl`), so the digest is left
154
+ empty and the local one is recorded as a note instead.
155
+
156
+ Imported lines arrive with **no owner**, which is what stops any authenticated
157
+ developer from publishing to them; `set-owner` hands one over.
158
+
159
+ ## Requires
160
+
161
+ `auth` (the User an owner and a publisher point at) and `apikeys` (the
162
+ `write_spl` Bearer token the publishing endpoint is guarded by).
163
+
164
+ `profile` is **optional**. It improves the credit on a publication and the name
165
+ shown on the public page, and the feature works without it.
166
+
167
+ ## Tests
168
+
169
+ ```bash
170
+ splent feature:test splent_feature_spl_registry
171
+ ```
172
+
173
+ Unit, integration and functional. The suite never reaches UVLHub: the conftest
174
+ replaces the four relay calls **and** the HTTP client underneath them, and it
175
+ does so through its own `MonkeyPatch` rather than the shared fixture. That last
176
+ detail is not decoration. While the shared fixture was used, a single
177
+ `monkeypatch.undo()` inside a test restored the real relay along with the real
178
+ `UVLHUB_API_KEY`, and a test run minted a permanent DOI on production Zenodo.
179
+ Publishing is irreversible, so the guard has to be one a test cannot lift.