- Python 92.5%
- HTML 7.4%
- Mako 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The feature had no license, so legally nobody could use, change or share it, which is what the marketplace exists for. It now carries the license of the SPLENT catalog, LGPL-3.0-or-later. A product that uses it can carry any license of its own, and changes to the feature itself stay open. COPYING.LESSER and COPYING hold the texts and pyproject.toml declares them, so the next release takes them to PyPI. |
||
| src | ||
| .gitignore | ||
| COPYING | ||
| COPYING.LESSER | ||
| MANIFEST.in | ||
| pyproject.toml | ||
| README.md | ||
splent_feature_spl_registry
The marketplace's registry of software product lines. It owns which SPLs exist, who owns each one, the DOI pointer to the published UVL model, and the relay that publishes a model to UVLHub.
Each line's name, description and {mirror, doi, file} pointer live in the
spl table, and GET /api/v1/spls serves them to the splent CLI and to the
job that builds the marketplace index.
The model
developer --(splent CLI with a splent token)--> MARKETPLACE --(one server-side key)--> UVLHub
A developer authenticates to the marketplace with a token from
splent_feature_apikeys and sends a .uvl file. They never hold UVLHub
credentials, never name a UVLHub dataset and never learn UVLHub is involved.
The marketplace validates the model, relays it with its single key, and stores
the DOI pointer. The UVL content itself is never stored here.
Publishing a FEATURE is unchanged: splent feature:release, no account, no
token, the git tag is the publication. This feature is only about product
lines.
Ordering
A release row is written before the irreversible call to UVLHub, then updated with the outcome. This is the point of the design. If the publish call times out and nothing was written down first, the marketplace cannot tell whether a DOI was minted, and the honest answer ("I do not know") gets replaced by the tempting one ("try again"), which mints a second permanent DOI for the same model.
A release is reserved while it is being published, and settles into one of
three states. Sending the same bytes again never mints a second DOI, whatever
the state:
| state | meaning | the same bytes sent again |
|---|---|---|
published |
UVLHub minted a DOI, recorded here | answered with that DOI |
failed |
UVLHub refused. Nothing was minted | sent again |
unknown |
No answer after the irreversible call | looked for on UVLHub first, answered with its DOI once found, refused until then |
A 5xx on the call that mints is unknown, not failed. A proxy in front of
UVLHub answers 504 when it stops waiting, while UVLHub carries on and mints.
Settlements are conditional updates. Whichever of the publication and the search below settles a release second never erases what the first learned. A late timeout never overwrites a DOI, and a DOI from UVLHub lands over a guess.
Answering at once, and finding a mint that lost track
Minting a DOI waits on Zenodo, and a new version is six Zenodo calls in a row,
which can take minutes. A client that sends Prefer: respond-async gets 202
as soon as the release is reserved, with a Location to follow it at, and the
relay runs in a thread with an application context of its own. A client that
does not ask gets the answer it always got, once the DOI exists. splent spl:publish asks, and follows the release until it settles.
A release nobody knows the outcome of, unknown or reserved for longer than
any call could take, is looked for on UVLHub. The registry walks the versions
of the line's dataset, downloads every DOI the line does not hold yet, and
settles the release as published only when the bytes UVLHub serves hash to
the sha256 recorded at reservation. Nothing short of that settles it. It looks
when the release's owner asks about it (in the background, at most once a
minute), when the same model is sent again, and when an administrator runs
splent feature:spl_registry reconcile. settle is left for the rare release
whose bytes UVLHub never serves.
Only one release of a line is published at a time. Each version is minted on
top of the one before, so a second model sent meanwhile is refused with 409.
Endpoints
| method | path | auth |
|---|---|---|
| GET | /api/v1/spls |
public |
| GET | /api/v1/spls/<name> |
public |
| POST | /api/v1/spls/<name>/releases |
Bearer, write_spl |
| GET | /api/v1/spls/<name>/releases/<version> |
Bearer, write_spl, the line's owner |
| GET | /marketplace/spls/<name> |
public page |
curl -X POST https://<marketplace>/api/v1/spls/my_line/releases \
-H "Authorization: Bearer splent_..." \
-H "Prefer: respond-async" \
-F "file=@my_line.uvl"
# 202, Location: /api/v1/spls/my_line/releases/3
curl https://<marketplace>/api/v1/spls/my_line/releases/3 \
-H "Authorization: Bearer splent_..."
# {"state": "published", "doi": "10.5281/zenodo...", ...}
Configuration
Every variable has a working default; the feature runs with no .env entry.
| variable | default | what it does |
|---|---|---|
UVLHUB_API_KEY |
(none) | the single server-side key. Without it the write path answers 503 |
UVLHUB_URL |
https://www.uvlhub.io |
which instance to relay to |
SPL_REGISTRY_PUBLISHING_ENABLED |
false |
the arming switch. Publishing answers 503 until a deployment turns it on, and every read path stays up either way |
SPL_REGISTRY_VERIFY_PUBLISHED |
true |
download the published file and compare it byte for byte |
SPL_REGISTRY_GATE_TIMEOUT_SECONDS |
10 |
how long a publication waits for the gate that serialises the quota count |
SPL_REGISTRY_UVLHUB_TIMEOUT |
60 |
seconds per UVLHub call |
SPL_REGISTRY_UVLHUB_PUBLISH_TIMEOUT |
600 |
seconds the two calls that mint a DOI may wait on Zenodo |
SPL_REGISTRY_QUOTA_WINDOW_HOURS |
24 |
the rolling window every quota is counted over |
SPL_REGISTRY_MAX_RELEASES_PER_USER |
10 |
per developer, per window |
SPL_REGISTRY_MAX_RELEASES_PER_SPL |
5 |
per product line, per window |
SPL_REGISTRY_MAX_RELEASES_TOTAL |
100 |
the ceiling that bounds the single key |
SPL_REGISTRY_MAX_SPLS_PER_USER |
5 |
product lines one developer may own |
UVLHUB_API_KEY is deliberately never copied into app.config: that mapping
is dumped by splent product:config, by the settings screens and by Flask's
debug pages. It is read from the environment at call time in exactly one place,
uvlhub.api_key(), which is the only place in the whole workspace that reads
it server side.
Quotas are counted with SQL COUNTs, never in-process counters, because
several workers run in parallel and a per-process counter would multiply every
allowance by the number of them.
403 or 404
Every product line is in the public index unless an administrator withdraws it
with visibility <spl> withdrawn. That one flag decides what the publishing
endpoint says about a line that is not yours:
| the line is | answer | why |
|---|---|---|
| listed | 403 |
admitting the name is taken tells the caller nothing they cannot read in GET /api/v1/spls |
| 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 |
Withdrawing hides a line. It deletes nothing: every release and every DOI stays exactly where it is, and the owner can still publish to it.
Authorship
A publication is credited to the publishing developer's profile, read server side and never from a request field, because an author line a stranger can choose is an impersonation vector on a permanent record.
The profile feature is not required. When the product installs it, the
credit is the name and surname on the developer's UserProfile, read
through UserProfileService.get_by_user_id. When it does not, or when the
developer never filled the form in, the credit is SPLENT MARKETPLACE. It is
never the email address, which would put a personal address on a permanent
public record.
A SPLENT profile holds a name and a surname and nothing else, so that is all that is sent. There is no affiliation and no ORCID to send.
Commands
splent db:seed
splent feature:spl_registry list
splent feature:spl_registry stuck [--hours 1]
splent feature:spl_registry reconcile [<spl>]
splent feature:spl_registry settle <spl> <version> [--doi ...] [--note ...]
splent feature:spl_registry set-owner <spl> <email>
splent feature:spl_registry visibility <spl> public|withdrawn
splent db:seed runs SplRegistrySeeder, which carries the founding product
lines as data. They were published before the registry existed, and their
name, description, mirror, DOI and file name are written out in seeders.py.
The seeder is what rebuilds the registry on a fresh database, and it is
idempotent.
It records no content digest for the lines it brings in. The digest is the
idempotency key, and a release carrying one is the registry promising these
exact bytes are what that DOI serves. Nothing has compared them, and one of the
three does not even serve a file named after the line
(sample_splent_spl serves sample_splent_app.uvl), so the digest is left
empty.
Seeded lines arrive with no owner, which is what stops any authenticated
developer from publishing to them; set-owner hands one over.
Requires
auth (the User an owner and a publisher point at) and apikeys (the
write_spl Bearer token the publishing endpoint is guarded by).
profile is optional. It improves the credit on a publication and the name
shown on the public page, and the feature works without it.
Tests
splent feature:test splent_feature_spl_registry
Unit, integration and functional. The suite never reaches UVLHub: the conftest
replaces the four relay calls and the HTTP client underneath them, and it
does so through its own MonkeyPatch rather than the shared fixture. That last
detail is not decoration. While the shared fixture was used, a single
monkeypatch.undo() inside a test restored the real relay along with the real
UVLHUB_API_KEY, and a test run minted a permanent DOI on production Zenodo.
Publishing is irreversible, so the guard has to be one a test cannot lift.