Releases: oddbit/shrtnr
py-v1.0.2
links.update()andbundles.update()can now distinguish "omit this field" from "set this field to null". Optional nullable params (label,expires_at,description,icon) default to a privateUNSETsentinel; only keys explicitly provided by the caller are included in the request body. PassNoneto clear a value, omit the param to leave the server-side value untouched. Sync and async resources both updated; regression tests cover both clients.
pub-v2.0.0
Breaking change to update() methods. LinksResource.update and BundlesResource.update now take a Link / Bundle object instead of an id with optional named parameters. To edit a record, call copyWith on the value returned by get() (or any other read), then pass it to update().
// 1.x
await client.links.update(42, label: null);
// 2.0
final link = await client.links.get(42);
await client.links.update(link.copyWith(label: null));Link, Bundle, and BundleWithSummary now expose a copyWith method. Pass null to a nullable field to clear it; omit the parameter to preserve the current value. The disambiguation between "omit" and "explicit null" is encapsulated in copyWith via a private sentinel, so callers never see it. update() always sends the full writable payload, which sidesteps the omit-vs-clear ambiguity at the wire level.
The Python and TypeScript SDKs keep their existing partial-update shapes; each language implements the same set/clear/omit feature in its own idiom.
npm-v1.0.2
qr()now acceptssizeas anumberinstead of astring. The API schema declares the parameter as an integer, so the previous string type forced callers to pre-stringify a number they already had. The method converts internally before appending to the query string.- Regression coverage added for null-clearing on
links.update(label, expiresAt) andbundles.update(description) so omit-vs-clear semantics are pinned down by tests.
app-v0.35.1
- Admin views that render "the link" (link-stat copy, link-create success state) now select the user's primary slug instead of the first auto-generated one. A
pickPrimarySlughelper centralizes the rule (is_primaryfirst, otherwise the lowest-id slug) so every surface picks the same slug for the same record. TIMELINE_RANGESis now the single source of truth for timeline-range tuples.RangeSchema,RANGE_VALUES, the admin time-range selector, analytics defaults, and bundle defaults all derive from this constant; the previous?? "30d"fallbacks scattered across files now reference an exportedDEFAULT_TIMELINE_RANGE. OpenAPI surface unchanged.- Runtime and dev dependencies refreshed to current published versions:
@cloudflare/workers-oauth-provider 0.4 → 0.5,@cloudflare/vitest-pool-workers 0.15 → 0.16,@cloudflare/workers-types 4.20260426 → 4.20260507,agents 0.11.6 → 0.12.3,hono 4.12.15 → 4.12.18,wrangler 4.86 → 4.88,zod 4.3 → 4.4. The full vitest suite (50 files, 889 tests) passes against the new versions.
app-v0.35.0
- Android (and iOS) in-app browser clicks now attribute to the originating brand in the Domains breakdown. The new
src/referrer.tsmodule maps known package identifiers (com.linkedin.android→linkedin.com,com.twitter.android→x.com, Facebook, Instagram, TikTok, Reddit, Pinterest, Slack, Discord, Telegram, WhatsApp, YouTube, Gmail, Outlook) to their canonical domain, so a click from the LinkedIn Android app shows up underlinkedin.cominstead of as an opaqueandroid-app://com.linkedin.android/. Uncurated packages fall through to "no referrer" rather than polluting the breakdown. - Sources panel (raw-URL referrer breakdown) now hides
android-app://andios-app://Referer values. The brand attribution lives in the Domains panel viareferrer_host; the raw row stays in the database for forensics and future re-attribution. Distinct-referrer counts are scoped consistently in both the link-detail and bundle analytics paths.
py-v1.0.1
Packaging, documentation, and CI hygiene. No public surface changes.
- Ship
NOTICEandTRADEMARK_POLICY.mdin the wheel and sdist alongside the existingLICENSE. Consumers running license scanners now see Oddbit's trademark policy without having to clone the repo. - README polish: dropped the "Migrating from 0.x" section and replaced "License" with an "Attribution" section that points at the same files.
- Internal: tightened return-type annotations on resource list methods so
mypy --strictpasses; aligned the e2e test fixture loop scope withpytest-asyncio's current default; cleared the remainingruffwarnings so CI runs clean. None of these change runtime behavior.
pub-v1.0.1
Packaging and documentation only. No public surface changes.
- Ship
LICENSE,NOTICE, andTRADEMARK_POLICY.mdin the package alongside the source. TheLICENSEfile now carries the standard Apache-2.0 text, replacing an earlier shorter placeholder;NOTICErecords third-party attributions andTRADEMARK_POLICY.mdcovers Oddbit's trademark policy. - README polish: dropped the "Migrating from 0.x" section and replaced "License" with an "Attribution" section that points at the same files.
npm-v1.0.1
Packaging and documentation only. No public surface changes.
- Ship
LICENSE,NOTICE, andTRADEMARK_POLICY.mdin the published tarball alongsidedist. Consumers running license scanners now see Apache-2.0 attribution and Oddbit's trademark policy without having to clone the repo. - README polish: dropped the "Migrating from 0.x" section (the 0.x line is past its rewrite window) and replaced "License" with an "Attribution" section that points at the same files.
app-v0.34.0
- Bundle access model now matches links and slugs: anyone with a valid API key can read a bundle and append links to it, and only the bundle owner can remove links, archive, unarchive, update, or delete. Non-owner write attempts return
403 Forbiddeninstead of the previous404 Not Found, so callers can tell "I don't have permission" apart from "this bundle does not exist". Read endpoints stay open across owners by design. - API contract tightened in three places.
urlon link create/update is capped at 2048 characters (aligned with IE 2083 and bit.ly 2000, long enough for typical UTM-laden URLs).slugonPOST /_/api/slugsnow matches the server-side validator exactly:[a-z0-9]at the start and end, hyphens allowed only in the middle, no underscores. Previously the regex was a permissive prefilter that accepted strings the service layer would then reject with a different error phrasing.expires_aton link create/update rejects negative Unix timestamps, which previously created links that read as already expired. - MCP server: removed
add_vanity_slug(and thevanity_slugparameter on link creation) in favor of the standalone slug tools. Addeddisable_slug,enable_slug, andremove_slugso an LLM can manage slug lifecycle the same way the admin UI does.search_linksandlist_links_by_ownerhonorrange, filter, and delta scoping consistent with the rest of the analytics surface;rangeis plumbed through every list and get tool to match the API. Tool schemas import the shared API schemas so the contract stays in lockstep with the server. - Slug rejection messaging clarified: requests that ask the server to "create a random slug" now state that explicitly instead of relaying the lower-level validation message.
py-v1.0.0
Ground-up rewrite derived from the OpenAPI spec. This is a deliberate breaking release.
Breaking changes
Resource-grouped client. All methods now live under client.links, client.slugs, or
client.bundles. Flat methods on the top-level client are gone.
# 0.x
client.create_link(CreateLinkOptions(url="..."))
client.archive_bundle(42)
# 1.0
client.links.create(url="...")
client.bundles.archive(42)Constructor shape. The positional base_url argument is replaced by a keyword-only base_url
parameter. api_key remains keyword-only.
# 0.x
Shrtnr("https://s.example.com", api_key="sk_...")
# 1.0
Shrtnr(base_url="https://s.example.com", api_key="sk_...")ShrtnrError shape. The body field is removed. Use server_message (the error string
from the JSON response). The str() representation formats as
"shrtnr API error (HTTP {status}): {server_message}".
Result types. delete, add_link, and remove_link return typed dataclasses
(DeletedResult, AddedResult, RemovedResult) instead of bare bool. Access
.deleted, .added, or .removed.
ClickStats expanded. New fields from the spec: referrer_hosts, link_modes, channels,
num_countries, num_referrers, num_referrer_hosts, num_os, num_browsers.
Link gains delta_pct?: click count change percentage versus the previous period.
BundleWithSummary is flat. Fields are directly on the object instead of nested under a
bundle attribute.
bundles.list archived parameter is now the raw spec enum string ("all", "only",
"1", "true") instead of a Python bool.
health() removed. The /_/health endpoint is outside the public API spec.
X-Client: sdk header removed. The 1.0 HTTP layer sends only Authorization: Bearer ....
New surface
client.links:get,list,create,update,disable,enable,delete,
analytics,timeline,qr,bundlesclient.slugs:lookup,add,disable,enable,removeclient.bundles:get,list,create,update,delete,archive,unarchive,
analytics,links,add_link,remove_linkAsyncShrtnrmirrors all methods withasync/await.
See the README for the full method table and migration guide.
1.0 post-release fixes (SDK review)
Bundle.accent is now required at parse time. Bundle.from_dict and
BundleWithSummary.from_dict previously used data.get("accent", "orange"),
masking a missing field. They now use data["accent"], raising KeyError if the
field is absent so the problem surfaces immediately.
bundles.list(archived=...) tightened to Literal["true","1","only","all"]
(was str), matching the TypeScript union type and completing cross-SDK parity.
Model names confirmed canonical. DateCount, SlugCount, BundleTopLink,
DeletedResult, AddedResult, RemovedResult are the reference names adopted by
all three SDKs.