simple_module_news
A self-contained news archive for SimpleModule hosts.
An article is a document this module owns outright: its title, slug, body, approval workflow, revisions, SEO and public URL are all columns and routes here. Install it, migrate it, and a host has a working archive — no other content module required.
It did not start that way. An article used to be a simple_module_pagebuilder
page, with this module contributing only a category and a display date beside
it. That made news uninstallable without its neighbour, made every listing a
cross-module join, and left article rows that could be orphaned by a deletion
news never saw. See Design note: what the split changed below.
Articles serve at /news/{slug} (the public_route_prefix setting), so an article
is distinguishable from a contact page in a URL, a log line and an analytics
report. The admin console is at /admin/news, not /news: one prefix cannot be
both a reader-facing URL and a permission-gated console.
Full documentation lives in docs/:
user guide (the admin screens and what readers see),
API reference (every route, permission and error),
operations (install, settings, scheduler, tenancy,
troubleshooting) and architecture.
Installation
pip install simple_module_news
Nothing else is required. For an in-repo checkout, resolve it from the workspace:
dependencies = ["simple_module_news"]
[tool.uv.sources.simple_module_news]
workspace = true
Then run make migrate — the module's first revision is labelled news, so it
can be removed on its own with alembic downgrade news@base.
Optional: pagebuilder
pip install "simple_module_news[pagebuilder]"
Where a host runs simple_module_pagebuilder too, two conveniences light up:
- the News feed block joins that module's palette, so a page can list live articles;
- the admin search screen gains Pages and Media sections alongside articles, with "see all" links into that module's screens.
Neither is required, and nothing outside news/integrations/pagebuilder.py
imports the package — every import there is deferred and guarded, so on a host
without it available() is False and the extra sections are simply absent.
test_module.py spawns a subprocess to prove that importing news never imports
pagebuilder.
One caveat, on the frontend. A bundler resolves imports statically, and that
includes dynamic ones — Rollup errors on an unresolvable import() exactly as
it does on a static import, so the try/catch around it never runs and no
amount of guarding inside this module makes the dependency conditional. So
news/puck-blocks.ts — and the NewsFeed component it pulls in — genuinely do
import @simple-module-py/pagebuilder at build time. They are the only two
files in this module's frontend that do, and @simple-module-py/pagebuilder is
declared an optional peer dependency so npm does not demand it. A host that
installs news without pagebuilder should exclude puck-blocks.ts from its block
glob (see host/client_app/blocks.ts); there is no feed block to register
without the palette it registers into. Every other screen — the list, both
editors, the public viewer — builds and runs with the package absent.
The real fix belongs to the framework rather than here: gen-pages already
emits the page globs, and could emit the block imports too, skipping a module
whose optional peers do not resolve — once, for every host and every module.
Usage
Go to News in the sidebar. "New article" creates the article and drops you into its body canvas — this module's own block editor — to write it. Headline, URL, category, tags, byline, display date and feed behaviour are edited on the article screen; the list edits category and date inline. A live article whose draft has moved on since it was published is marked Unpublished edits in the list, because "Published" on its own is a true sentence about a document nobody is looking at.
The article screen also carries Review (submit, approve, send back with a note), Schedule, Search & sharing and History. Binned articles are under News → Trash, where they can be restored or removed for good.
The two screens split by posture, not by importance: the canvas wants the whole viewport and autosaves on a timer, and the article screen is a column of short fields saved by one button. The headline lives on the article screen rather than on the canvas because moving it also moves the URL, and a rename is a decision — not something that should happen on a debounce.
The reader's side
/news/ is the archive: published articles, newest first, twelve to a page,
with /news/category/{slug}, /news/tag/{slug} and /news/author/{slug}
narrowing it, ?q= searching within whichever of those you are on, and
/news/feed.xml carrying the recent run as RSS.
Search narrows the page it was typed on rather than replacing it, which is why
there is no /news/search: a reader on a category page is standing somewhere
that already promises a scope, and answering a question they did not ask by
silently widening to the whole archive is worse than answering the one they
did. Results are noindex, follow — the input space is unbounded, so one
indexed ?q= invites a crawler to enumerate query strings forever, while the
links out of a result page are the real documents and worth following.
It searches headline, slug, excerpt and tags, and deliberately not the
body. Not for cost: the body column the admin's search scans is draft_data,
so a public search over it would answer for text nobody has published — a
phrase living only in an unpublished edit would surface the article and tell an
outsider that the edit exists.
An author becomes an address through the same slug rule as everything else, so
two spellings of one byline (A. Subash, A Subash) share one page rather
than one of them disappearing: that is nearly always one person entered
inconsistently, and two genuinely different people are an editorial fix that
costs nothing. A byline with no ASCII to fold onto has no address at all and
renders as plain text — a missing feature, chosen over the slugifier's fallback
collapsing unrelated writers onto a single archive claiming to be each of them.
None of that existed until recently, and its absence was the hole in the middle
of the split. The public router had exactly one route — /{slug} — so a reader
could open an article they already had a link to and nothing else: /news/
answered 404, the bare /news bounced an anonymous visitor to the sign-in
screen, and the only browsing surface in the codebase was the News feed
block, which registers into pagebuilder's palette. A module that could be
installed alone could not be read alone.
Paging is real links rather than "load more", because the pager is how a
crawler reaches everything past the first page. A page beyond the end is a 404
rather than an empty document, so a crawler guessing ?page=900 is told there
is nothing there instead of being handed something valid-looking to index.
The first page of a narrowing that matches nothing is the opposite case, and the two are easy to confuse. An unknown tag, a byline nobody wrote under, or a search with no hits all render an empty archive with a way back: the address is meaningful and simply holds nothing today — a tag can be removed from the last article carrying it, and a URL published while it existed should say "nothing here now", not "never existed". Page 900 of that same empty tag is still a 404, because there is no reading under which it has one.
Metadata a crawler can read
Articles and archive pages carry a server-rendered <head> — <title>, the
description, og:*, twitter:*, the canonical link, robots and JSON-LD —
written into the document before it leaves the server.
They have to be, because the host renders one Inertia shell for every screen: a
fixed <title> and an {% inertia_head %} slot that only fills under SSR,
which nothing configures. PublicArticle.tsx composes all of those tags through
Inertia's <Head>, and every one of them existed only after the bundle ran.
Google executes JavaScript; Slack, X, LinkedIn, Facebook and most feed tooling
do not — so an article link previewed as "SimpleModule" with no description and
no image, and an article marked index_in_search=false was indexed anyway
because the noindex never reached the crawler either.
endpoints/public/_head.py writes them in on the way out. The tags are
duplicated with the client-side ones on purpose: one copy serves the crawler
that never runs the script, the other serves the reader who arrived through the
SPA and never reloaded, and neither covers the other's case. If the host ever
adopts SSR, {% inertia_head %} starts emitting the same tags and that module
should be deleted rather than made cleverer.
Permissions
| Permission | Grants |
|---|---|
news.view |
see the admin list |
news.edit |
create, write and edit articles; submit for review |
news.publish |
publish, unpublish, approve, reject, purge, hard delete |
news.publish is separate on purpose. Publishing used to require
pagebuilder.publish, because the write landed on one of its pages and news was
not entitled to route an author past that module's editor → publisher
separation. Owning the content means owning the separation: folding it into
news.edit would hand every author a way past a review step a host may want.
API
| Route | Access |
|---|---|
GET /news/ |
anonymous; the archive, paged |
GET /news/category/{slug} |
anonymous |
GET /news/tag/{slug} |
anonymous |
GET /news/{slug} |
anonymous; published articles only |
GET /{locale}/news/{slug} |
anonymous; one mount per non-default content locale |
GET /news/feed.xml |
anonymous; RSS 2.0, most recent 20 |
GET /news/sitemap.xml |
anonymous |
GET /api/news/articles?limit&offset&category&q&status&in_feed&locale&translation_group&undated_first |
anonymous; published only |
GET /api/news/categories |
anonymous; published only |
GET /api/news/articles/{id}/tags |
anonymous; published only |
POST /api/news/articles |
news.edit |
POST /api/news/articles/{id}/translations |
news.edit |
PUT /api/news/articles/{id} |
news.edit |
DELETE /api/news/articles/{id} |
news.edit + news.publish |
GET /api/news/articles/{id}/detail |
news.edit |
PUT /api/news/articles/{id}/body |
news.edit |
GET /api/news/articles/{id}/revisions |
news.edit |
POST /api/news/articles/{id}/revisions/{rev}/restore |
news.edit |
POST /api/news/articles/{id}/submit |
news.edit |
POST /api/news/articles/{id}/trash | /restore |
news.edit |
POST /api/news/articles/{id}/publish | /unpublish |
news.edit + news.publish |
POST /api/news/articles/{id}/schedule |
news.edit + news.publish |
POST /api/news/articles/{id}/approve | /reject |
news.edit + news.publish |
DELETE /api/news/articles/{id}/purge |
news.edit + news.publish |
Reads are anonymous because the feed block runs on public pages. Listing is
ordered pinned first, then newest, with undated articles last;
undated_first=true flips the undated to the front, which is what the admin
list uses so work in progress is not buried on the last page. An editor
additionally sees drafts. Each item carries status, which the admin list
renders as a badge; without news.edit it is always published.
An editor's items also carry has_unpublished_changes — the article is live
and its draft has since moved on, so what you are looking at is not what
readers are served. Only for a caller who may see drafts: it is a statement
about work in progress, and the column behind it is not even selected for
anyone else. It is compared, not inferred from a timestamp, and it is false for
a draft holding an old snapshot — there is nothing live there to diverge
from.
POST /articles creates the article in one insert. Omit slug and the server
derives one from the title and takes the first free variant — my-title, then
my-title-2; send one and it is used verbatim, with a collision reported as a
409 rather than silently renamed. Trashed articles keep their slugs claimed.
PUT /articles/{id} is a partial update covering the listing metadata, the
identity (title, slug) and the SEO fields. A field you omit is left alone.
Sending published_at as an explicit null is different from omitting it —
that undates the article, which is a real state rather than an error. Changing
slug records a redirect, so the old address keeps working.
PUT /articles/{id}/body is the canvas autosave, and deliberately cannot reach
the slug or the status: it fires on a timer rather than on a person pressing
something.
Scheduling
POST /articles/{id}/schedule sets publish_at and unpublish_at: a draft
goes live by itself at the first, a published article comes down by itself at
the second. Both are optional and both are three-valued — an omitted field is
left alone, an explicit null cancels. Behind news.publish, because a
schedule is a publication decision that happens to be about the future.
An in-process loop (scheduler_enabled, every scheduler_interval_seconds)
calls ArticlesService.process_due. The query is <= now rather than "since
the last tick", so a process that was asleep catches up rather than losing the
window; each timestamp is cleared when acted on, so a tick cannot republish the
same article forever.
Running more than one of them
The loop starts in every process that boots the module, so uvicorn -w 4,
gunicorn with workers and a Deployment with replicas > 1 all run several. That
is safe: before touching a due article, process_due claims it with one
conditional statement whose own effect takes it out of the due set —
UPDATE news_article SET publish_at = NULL
WHERE id = :id AND deleted_at IS NULL AND status = 'draft'
AND publish_at IS NOT NULL AND publish_at <= :now
— and acts only if that updated a row. The check and the claim are one
statement, so the database decides the winner: whichever replica runs second
meets a row that no longer matches and moves on. One PUBLISH revision row per
article, and no flip-flop on the way down. An external worker driving
process_due alongside the loop is safe for the same reason.
Row claiming rather than SELECT … FOR UPDATE SKIP LOCKED, which is
Postgres-only and would do nothing on SQLite, this repo's local-dev default; and
rather than a lease table, which elects one scheduler for the whole app and buys
a migration, a clock comparison and a window after a leaseholder dies in which
nothing publishes. A claim here lives in the tick's transaction, so a process
that dies mid-flip releases it and the article is simply due again. See
news/content/_claims.py.
What it does not cover: N replicas still each poll the database every
scheduler_interval_seconds, and each still reads its own clock — an article
goes live when the first replica to think it due acts, so a badly skewed clock
moves that by the skew, exactly as the poll interval already does. Where either
matters, set scheduler_enabled false everywhere and drive process_due from
one place — a cron job, a k8s CronJob, a dedicated worker. Set it on the
Settings screen or with scripts/set_setting.py news scheduler_enabled false,
never an environment variable, which this module does not read.
These are deliberately not published_at, which is the display date below
and may perfectly reasonably be in the past. The article screen said "a future
date lists this as scheduled" for a while, which read as a promise nothing kept.
published_at is a display date, not a timestamp. Whatever instant you
send, the day is taken as you wrote it and stored as midnight UTC — sending
2026-02-01T23:00:00-06:00 records the 1st, not the 2nd. It is also independent
of status: an article can be published and undated, or dated and still a
draft.
Multilingual articles
Every article carries its own language (locale), fixed for its lifetime, and
shares a translation_group with its translations. The languages on offer are
pagebuilder's content_locales (pages and articles share one list; without
pagebuilder the site is monolingual) — see
docs/operations.md. Off by default: with one content locale every article URL is exactly what
it was.
Articles in the default language keep /news/{slug}; every other language is
prefixed, /de/news/{slug}. ArticleRead.url carries the prefix, so the admin
list, the feed block and the slug news claims from pagebuilder all agree on
one address. /{default}/news/{slug} permanently redirects to the bare form.
?locale=de narrows a listing to one language — what a feed block on a German
page passes, so a German list never shows an English card. The block reads the
surrounding page's language rather than offering it as a field: a feed set to
one language on a page written in another is a mistake nothing would catch.
The admin list leaves it unset and shows every language, badged per row.
POST /articles/{id}/translations starts the same story in another language:
one request, one transaction, creating both the translated page and the sidecar
row. Category, byline, date, pin and feed membership are copied from the source
rather than asked for again — they are facts about the story, not about the
language it is told in. The translation starts as a draft.
?translation_group=… lists one article and its counterparts, which is what
the editor's language switcher shows. It goes through the ordinary listing
rather than a route of its own, so it inherits the same visibility rule: a
reader without news.edit sees the published translations only.
Anonymous listings carry Cache-Control: public, max-age=60, because the feed
block runs on every public page that holds one. An editor's listing includes
drafts and so is private, no-store. The article viewer sends an ETag,
Cache-Control: public, max-age=300, stale-while-revalidate=60, and
Content-Security-Policy when public_csp is set.
The console's language
Separate from the article's. The section above is about what the site
publishes; this is about what the console speaks, and the two are configured
in different places — content_locales for one, the host's
SM_I18N_SUPPORTED_LOCALES for the other.
Every console string lives in news/locales/en.json, registered by
NewsModule.locale_dirs() under the news namespace, so a key reads
news.<section>.<name> in the merged catalogue the host ships to the browser.
Add a language by adding news/locales/<tag>.json with the same shape; the
framework's SM013–SM016 diagnostics report a file that has drifted from the
default.
The frontend derives its key tree from that JSON at compile time —
news/utils/i18n.ts — rather than from the framework's generated keys, which
only carries the namespaces of modules that live in the framework's own
monorepo. The upshot is that tsc rejects a key the catalogue does not define,
and modules/news/news/utils/i18n.test.ts rejects an entry nothing references,
a plural missing a form, and a placeholder nothing fills.
Two things stay literal on purpose. A block's defaultProps are the seed
content a writer is given and are then saved into the document, so
translating them at render time would rewrite what a reader is served. And
Puck's block labels are catalogue keys in the config constant, resolved by
localizeConfig inside the screen that mounts the editor — a module-scope
config cannot call a hook, and puck-blocks.ts is imported before the app has
rendered anything.
The feed block is localized by its neighbour. NewsFeed lands in
pagebuilder's palette, not this module's, so the resolver is pagebuilder's
localizeConfig when PageEditor mounts Puck — it walks the assembled config,
registered blocks included, and resolves whatever keys it finds there. That
works across the module boundary because the host merges every catalogue into
one flat table and i18next runs with keySeparator: false, so news.feed.… is
a whole key rather than a path into a namespace pagebuilder would have to know
about. The pagebuilder extra carries a >=0.0.8 floor for exactly this: an
older neighbour does not crash, it renders each label as its own dotted key.
Settings
Stored in the database and edited under Settings → News. There is no
SM_NEWS_* environment variable: the settings class drops every env source, so
a value can only come from the store or from the default below. Headless
deployments write them with scripts/set_setting.py.
Fields marked ● are read once while the app boots — they decide which routes are mounted and what the auth layer exempts — so changing one needs a restart. The Settings screen says so next to the input.
| Setting | Default | What it does |
|---|---|---|
public_route_prefix ● |
/news |
where articles serve |
public_base_url |
(derived from the request) | origin for canonical URLs and the sitemap |
site_name |
(unset) | og:site_name |
twitter_handle |
(unset) | twitter:site |
public_cache_max_age |
300 |
shared-cache lifetime of an article |
public_cache_swr |
60 |
stale-while-revalidate seconds; 0 omits it |
public_csp |
(unset) | Content-Security-Policy on the article page |
scheduler_enabled ● |
true |
run the in-process publish/unpublish loop — safe under replicas, which claim each due article; see Scheduling |
scheduler_interval_seconds ● |
30 |
how often it looks for due articles |
The languages articles may be written in are not here. They are
pagebuilder's content_locales, borrowed through news.integrations.locales
so one site does not keep two lists that can disagree. Without pagebuilder news
publishes in one language, at the unprefixed URL — the behaviour every
monolingual site already has.
Design note: what the split changed
The sidecar was a real design with real reasons — it kept pagebuilder a generic CMS that knew nothing about news, and it meant articles inherited a mature editor, viewer and workflow for free. What it cost was independence, and three specific hazards that are now structurally impossible:
- No orphans.
news_articles.page_idwas an unenforceable pointer into another module's table: the framework gives every module its ownMetaData, so a cross-moduleForeignKeycould not resolve. A deleted page left a row behind, and SQLite reuses ids, so that row would re-attach to whatever page was created next and list one article's metadata against another's document. APageDeletedsubscription and a startup sweep existed only to contain this. Both are gone; an article's body is its own row. - No cross-module join. Every listing joined
pagebuilder_pagesand had to rememberload_onlyto avoid dragging both block-JSON columns through the ORM per row. The listing is now a single-table scan over named columns. - A real permission boundary. Publishing borrowed
pagebuilder.publish; it isnews.publishnow, on this module's own routes.
Two things news gained that it previously borrowed, and had to grow itself:
- Its own viewer, carrying the ETag, cache, CSP, canonical tag and old-slug
redirects the hand-off used to provide.
test_public_address.pyasserts each one, precisely because a second viewer is what the hand-off existed to avoid. - Its own sitemap, at
{prefix}/sitemap.xml. Articles used to reach a crawler through pagebuilder's, via a claim news registered with it; without one of its own the whole archive would silently drop out of every index. It lists the archive index and every category and tag page as well as the articles — a sitemap of leaves says the articles exist but not that anything links them — and a taxonomy page only while at least one published, indexable, listed article in that language fills it. An empty one is a thin page, and a sitemap is a request to come and index.
One thing it deliberately did not grow: a clone of pagebuilder's widget catalogue. That catalogue builds pages — heroes, feature grids, site headers — and almost none of it belongs in a news story. The body canvas ships a prose palette instead; see The block palette below. A host that wants the full page-building set on an article can install pagebuilder, build the page there, and link to it.
The block palette
Grouped in the editor by what a writer is reaching for, which is not always what the block renders — Key points and Sources are both lists, and neither sits beside List, because someone wanting a summary box is not shopping for a list.
| Group | Blocks |
|---|---|
| Text | Heading, Paragraph, List, Pull quote, Q&A |
| Set apart | Key points, Callout, Definitions, Sources, Read next |
| Media | Image, Gallery, Before / after, Video file, Audio clip, Embed |
| Data | Table, Key figures, Code, Timeline |
| Layout | Divider |
The bar for adding one is that a newsroom already does this thing in a story and currently has to fake it with a paragraph — a summary box, a correction, a table of figures, a dated sequence, a list of sources. A block that only changes how something looks is a page-building widget wearing a different hat, and belongs in the other module.
A few consequences of that bar worth knowing:
- Callout has a
correctionkind. A correction is an obligation a publication owes its readers, and one written as an ordinary paragraph is indistinguishable from the reporting it corrects. - Image carries a
creditseparately from its caption, because they are different obligations — a caption explains the picture, a credit says whose it is, and a publication that runs the second inside the first eventually runs a picture with neither. - Timeline takes its dates as free text. Reporting deals in "March 2024", "the following morning" and "some time before 2019"; a calendar control would force a writer to invent precision the reporting does not have.
- Code is not syntax-highlighted and its
languageis a label for the reader, not a hint to a parser. Highlighting means shipping a highlighter, and these render on a host that may have installed nothing else. - Gallery is a grid, not a carousel — a carousel hides all but one image behind an interaction, costing a reader who is scrolling the pictures they were shown.
- Divider offers an asterism (
* * *) as well as a rule. They mean different things: a rule separates the article from something appended to it, an asterism marks a change of scene within one continuous piece. - Q&A and Definitions render as
<dl>rather than as alternating paragraphs, because that is what they are — each question introduces the answer that follows it, and a screen reader announcing the pairing gives a listener the structure a sighted reader gets from the indent. - Before / after stays two frames side by side on a phone rather than stacking, and is not a drag-the-handle slider. A comparison a reader has to scroll between is one they have to hold in their head; a slider hides half of each frame behind an interaction the reader may never make.
- Video file and Audio clip are for media the publication hosts itself.
Embedis an<iframe>and so only speaks to services that publish a player; a reporter's own mp4 had nowhere to go before these existed. Bothpreload="metadata", because an article can carry several and a reader who scrolls past one should not have paid for it.
Read next is the exception
Every other block renders exactly what a writer typed into it. Read next queries the listing API instead, because a related-articles list hand-typed at publication is stale the moment the next article goes up and nobody returns to a three-month-old story to refresh it.
That makes it the one block that has to know which article it is inside — a
"read next" offering the article you are reading is visibly broken. The viewer
passes the current slug to <Render> as Puck metadata, and the block reads it
from puck.metadata.currentSlug. That seam is deliberately general: it is where
any future block that needs the article rather than its own props should look.
It renders its own list rather than reusing the card grid the NewsFeed block
draws with, because that grid belongs to pagebuilder and this has to work
without it.
Blocks whose content is a list — List, Key points, Q&A, Definitions, Sources,
Timeline, Key figures, Gallery, Table — are edited as a textarea, one item per
line, rather than through Puck's array field, so a writer pasting a list out of
a document gets it in one action instead of clicking "add item" nine times.
blocks/lines.ts is that parsing, in one place, so no two blocks can disagree
about what a line means.
The palette also carries a Chart and a Contents (table of contents) block; docs/user-guide.md lists every block by group.
Known gaps
-
The article body still needs JavaScript. The metadata does not — see The reader's side — but the document itself is a block tree rendered by React, so the raw HTML carries the article's text only inside the Inertia data blob. A crawler that executes scripts reads it; one that does not sees the head and nothing else. Server-rendering the body would mean a Python implementation of all twenty-one blocks, or SSR in the host. Both are real projects; neither is this module's to start.
-
Images take a URL, not a picker. The media library belongs to pagebuilder, and the Image and Gallery blocks have to work without it. The single-URL fields now show what the address points at as it is typed, so a wrong one is caught in the panel rather than on the published page — but that is a preview, not a library, and the extra step remains.
The obvious alternative does not work. This repo's host also runs
file_storage, but every one of its routes sits behindRequiresPermission, there is no static mount overuploads/, andStoredFileOutcarries nourlat all. An article image served from it renders in the admin preview and 401s for every logged-out reader — which is exactly why pagebuilder mountsMediaFiles(StaticFiles)and adds an auth-exempt prefix for its own media, saying so inboot.py: "without this the page renders for an anonymous visitor but every image 302s to login." Proxying it through a news route would be worse than the gap, not a shortcut: the store is shared by every module with no notion of "public", so aGET /media/{file_id}would make any file anyone ever uploaded anonymously readable to whoever holds the UUID. The fix is a public-read capability infile_storage, which is the framework's to add. -
The scheduler still polls per process. Publishing at the right moment is now safe with several replicas — each due article is taken with one conditional
UPDATE, so two ticks landing together flip it once — but every replica still wakes on its own interval. Where that traffic is unwanted, turningscheduler_enabledoff and drivingprocess_duefrom one external worker remains the answer. That is now a preference rather than a safety requirement.
Development
uv sync --extra dev
uv run pytest
API-version contract
ModuleMeta.requires_framework declares which simple_module_core versions
this module supports. Update the spec on each framework major bump after
verifying compatibility.
Metadata
Release files for simple-module-news 0.0.11
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| simple_module_news-0.0.11.tar.gz | 370.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| simple_module_news-0.0.11-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 687.9 kB
Release files / simple_module_news-0.0.11.tar.gz
| Download URL | simple_module_news-0.0.11.tar.gz |
|---|---|
| Size | 370.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d897d6aa73fa466f297cbb26ab87b3cd7889c86118ee9976a6c4b2feebcc4a5e
|
|
BLAKE2b-256 checksum How to use checksums |
08a95c2cfd2879f7c148f64feba99564ebd774aa5d524d3b40f0a1e69c9c7ab2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency logRelease files / simple_module_news-0.0.11-py3-none-any.whl
| Download URL | simple_module_news-0.0.11-py3-none-any.whl |
|---|---|
| Size | 317.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e64c0902192ed9aeaf1aa40d4d2362bfcf62b84a9404fb611fa8325bd577ca1a
|
|
BLAKE2b-256 checksum How to use checksums |
f663aaf00f375de2b85a19a4af4187ebbb49f862a9e95add95113ebcd5917544
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency log