Skip to content

Releases: oddbit/shrtnr

py-v1.0.2

07 May 06:21
8319c01

Choose a tag to compare

  • links.update() and bundles.update() can now distinguish "omit this field" from "set this field to null". Optional nullable params (label, expires_at, description, icon) default to a private UNSET sentinel; only keys explicitly provided by the caller are included in the request body. Pass None to 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

07 May 06:21
8319c01

Choose a tag to compare

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

07 May 06:21
8319c01

Choose a tag to compare

  • qr() now accepts size as a number instead of a string. 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) and bundles.update (description) so omit-vs-clear semantics are pinned down by tests.

app-v0.35.1

07 May 06:20
8319c01

Choose a tag to compare

  • 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 pickPrimarySlug helper centralizes the rule (is_primary first, otherwise the lowest-id slug) so every surface picks the same slug for the same record.
  • TIMELINE_RANGES is 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 exported DEFAULT_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

01 May 03:07
25af9a8

Choose a tag to compare

  • Android (and iOS) in-app browser clicks now attribute to the originating brand in the Domains breakdown. The new src/referrer.ts module maps known package identifiers (com.linkedin.androidlinkedin.com, com.twitter.androidx.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 under linkedin.com instead of as an opaque android-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:// and ios-app:// Referer values. The brand attribution lives in the Domains panel via referrer_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

30 Apr 05:33
fed2cb3

Choose a tag to compare

Packaging, documentation, and CI hygiene. No public surface changes.

  • Ship NOTICE and TRADEMARK_POLICY.md in the wheel and sdist alongside the existing LICENSE. 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 --strict passes; aligned the e2e test fixture loop scope with pytest-asyncio's current default; cleared the remaining ruff warnings so CI runs clean. None of these change runtime behavior.

pub-v1.0.1

30 Apr 05:33
fed2cb3

Choose a tag to compare

Packaging and documentation only. No public surface changes.

  • Ship LICENSE, NOTICE, and TRADEMARK_POLICY.md in the package alongside the source. The LICENSE file now carries the standard Apache-2.0 text, replacing an earlier shorter placeholder; NOTICE records third-party attributions and TRADEMARK_POLICY.md covers 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

30 Apr 05:33
fed2cb3

Choose a tag to compare

Packaging and documentation only. No public surface changes.

  • Ship LICENSE, NOTICE, and TRADEMARK_POLICY.md in the published tarball alongside dist. 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

30 Apr 05:32
fed2cb3

Choose a tag to compare

  • 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 Forbidden instead of the previous 404 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. url on link create/update is capped at 2048 characters (aligned with IE 2083 and bit.ly 2000, long enough for typical UTM-laden URLs). slug on POST /_/api/slugs now 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_at on link create/update rejects negative Unix timestamps, which previously created links that read as already expired.
  • MCP server: removed add_vanity_slug (and the vanity_slug parameter on link creation) in favor of the standalone slug tools. Added disable_slug, enable_slug, and remove_slug so an LLM can manage slug lifecycle the same way the admin UI does. search_links and list_links_by_owner honor range, filter, and delta scoping consistent with the rest of the analytics surface; range is 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

29 Apr 09:04
e26ae15

Choose a tag to compare

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, bundles
  • client.slugs: lookup, add, disable, enable, remove
  • client.bundles: get, list, create, update, delete, archive, unarchive,
    analytics, links, add_link, remove_link
  • AsyncShrtnr mirrors all methods with async/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.