Skip to main content

phrappy

PyPI Downloads

Typed, batteries-included Python client for Phrase TMS (Memsource) generated from the public OpenAPI spec. Comes with both sync and async clients, fully equipped with first-class Pydantic v2 models.

The build process is fully automated and project release is planned to follow the Phrase TMS bi-weekly release cadence.

This project is not an official Phrase/Memsource SDK. Official documentation can be found at developers.phrase.com


Installation

pip install phrappy

Requirements: Python ≥ 3.10


Quickstart

1) Authenticate to get a token

Either use your authentication method of choice directly to get a token.

from phrappy import Phrappy
from phrappy.models import LoginV3DtoV3

pp = Phrappy()
login_response = pp.authentication.login_v3(login_v3_dto_v3=LoginV3DtoV3(
    userName="your_name",
    password="<password>"
))
token = login_response.token
pp.close()

All typed methods also accept dict inputs that are then validated under the hood. For example:

from phrappy import Phrappy


pp = Phrappy()
login_response = pp.authentication.login_v3(login_v3_dto_v3={
    "userName":"your_name",
    "password":"<password>"
})
token = login_response.token
pp.close()

Or use the convenience method for authenticating and getting a Phrappy instance that carries its token.

from phrappy import Phrappy

pp = Phrappy.from_creds(username="name@example.com", password="…")
me = pp.authentication.who_am_i()
print(me.user.uid)
pp.close()

For a 2-factor-enabled account, or to log in as a specific user of an identity:

pp = Phrappy.from_creds(
    username="name@example.com",
    password="…",
    code="123456",          # 2FA verification code
    user_uid="…",           # defaults to the identity's default user
)

Using a context manager closes the underlying HTTP client automatically:

from phrappy import Phrappy

with Phrappy(token="<YOUR_TOKEN>") as pp:
    me = pp.authentication.who_am_i()
    print(me.user.userName)

2) Or authenticate with OAuth 2.0

Phrase TMS supports a standard OAuth 2.0 authorization code grant alongside token auth, and its access tokens go out as Bearer rather than ApiToken. Pass auth_scheme="Bearer" with the access token your flow obtained:

from phrappy import Phrappy

pp = Phrappy(token="<OAUTH_ACCESS_TOKEN>", auth_scheme="Bearer")
me = pp.authentication.who_am_i()
print(me.user.userName)
pp.close()

A token that already carries its scheme is used as given, so Phrappy(token="Bearer …") works without configuring anything:

from phrappy import Phrappy

with Phrappy(token="Bearer <OAUTH_ACCESS_TOKEN>") as pp:
    me = pp.authentication.who_am_i()
    print(me.user.uid)

phrappy does not perform the authorization redirect or the code exchange — obtaining the access token is your application's job. Register the app under Settings → Integrations → Registered OAuth Apps to get a Client ID, then use https://cloud.memsource.com/web/oauth/authorize and https://cloud.memsource.com/web/oauth/token. Phrase's flow is a public-client grant validated by redirect URI alone: no client secret and no PKCE. OAuth login is not supported for users belonging to more than one organization.

3) Async usage

import asyncio
from phrappy import AsyncPhrappy

async def main():
    async with AsyncPhrappy(token="<YOUR_TOKEN>") as app:
        me = await app.authentication.who_am_i()
        print(me.user.userName)

asyncio.run(main())

Examples

Create a project and upload a job (multipart)

from pathlib import Path
from phrappy import Phrappy, cdh_generator
from phrappy.models import CreateProjectV3DtoV3, JobCreateRequestDtoV1

with Phrappy(token="<YOUR_TOKEN>") as pp:
    proj = pp.project.create_project_v3(
        create_project_v3_dto_v3=CreateProjectV3DtoV3(
            name="Demo", sourceLang="en", targetLangs=["sv"]
        )
    )

    p = Path("example.txt"); p.write_text("Hello from phrappy")
    jobs = pp.job.create_job(
        project_uid=proj.uid,
        content_disposition=cdh_generator(p.name),
        file_bytes=p.read_bytes(),
        memsource=JobCreateRequestDtoV1(targetLangs=proj.targetLangs),
    )
    print([j.uid for j in jobs.jobs or []])

List your assigned projects

me = pp.authentication.who_am_i()
page = pp.project.list_assigned_projects(user_uid=me.user.uid, target_lang=["sv"])  # typed page model
for item in page.content or []:
    print(item.name, item.status)

API design

  • Typed models everywhere! Inputs/outputs are Pydantic v2 models generated from the OpenAPI. You can pass either a model instance or a dict for body/header parameters; the client will validate and coerce.
  • All generated operations are keyword-only. Parameter order is derived from the spec and shifts when Phrase changes which fields are required, so positional calls would silently rebind. Passing arguments by name makes that impossible.
  • Rich method docstrings based on operation descriptions and typing information.
  • Every operation exists in both Phrappy and AsyncPhrappy under the same tag-based namespaces.
  • Built on httpx with httpx.Client/httpx.AsyncClient under the hood.
  • Response models are lenient by default: unexpected fields in a Phrase TMS response are accepted and retained on the model (Pydantic extra="allow") rather than raising ValidationError. This shields production callers from benign upstream drift when the API response diverges from the documented schema. Request models stay strict so caller typos fail fast.
  • Set PHRAPPY_STRICT=1 in the environment before importing phrappy to flip response models to extra="forbid" for CI drift detection. The setting is read once at import time; per-call toggling is not supported.

If you find a mismatch between the API behavior and the generated models, please open an issue with the request/response payloads (redacted) and the package version.


Configuration

  • Defaults to https://cloud.memsource.com/web. Override via Phrappy(base_url=...) or AsyncPhrappy(base_url=...). Profiles in the US data centre need https://us.cloud.memsource.com/web.
  • Pass timeout= (seconds) to the client constructor. Per-request timeouts are also supported on make_request if you wrap custom calls.
  • auth_scheme= sets the Authorization scheme used for a bare token. Defaults to "ApiToken"; pass "Bearer" for OAuth 2.0 access tokens. A token that already starts with a recognised scheme is sent unchanged, and the same normalisation applies to a per-call phrase_token= override.

Testing

The test suite has three layers:

  1. Offline fixture tests — captured Phrase TMS response bodies replayed through the generated Pydantic response models. Runs by default, no network, fast.
  2. Drift detection — same fixtures re-run under PHRAPPY_STRICT=1 to surface upstream schema divergence (unexpected fields, etc.) that lenient mode would silently tolerate.
  3. Live tests — end-to-end against a real Phrase TMS account. Creates and deletes real resources, costs a handful of words per run.
# 1) offline suite (default — no credentials needed)
pytest -m "not live and not destructive" -q

# 2) drift detection — re-runs the offline suite under PHRAPPY_STRICT=1
scripts/run-strict-tests.sh

# 3) live tests — will create and delete assets in your account!
export PHRAPPY_TOKEN='ApiToken ...'
pytest -m live -q

Strict mode is gated by tests/test_phrappy/fixtures/_drift_allowlist.json, a hand-maintained list of fixture paths with known upstream schema drift (fields Phrase returns that the OpenAPI spec doesn't declare). The strict suite asserts both directions:

  • Fixtures on the allowlist MUST raise ValidationError in strict mode. If upstream fixes one, the test fails so the allowlist stays honest.
  • Fixtures not on the allowlist MUST validate cleanly. A new drifter fails loudly instead of being lost in the noise.

Env vars used by the live tests and the fixture-capture helper:

  • PHRAPPY_TOKEN or (PHRAPPY_USER and PHRAPPY_PASSWORD)
  • PHRAPPY_BASE_URL (optional)

Fixtures under tests/test_phrappy/fixtures/ are captured by scripts/capture_fixtures.py (see its --dry-run output for what it would capture). New fixtures are redacted of user-identifying fields before they are committed.


Releasing

Releases are automated by .github/workflows/release.yml. The short version:

  1. Bump __version__ in src/phrappy/_meta.py.
  2. Add a ### X.Y.Z section to the ## Release notes below.
  3. Commit to main, run pytest -m "not live and not destructive" locally.
  4. git tag -a vX.Y.Z -m "phrappy vX.Y.Z" && git push origin main && git push origin vX.Y.Z.

The tagged push triggers regen + tests + python -m build + PyPI publish (OIDC trusted publishing) + sync to kuhnemann/phrappy + GitHub Release. Full runbook and the one-time credential setup are in tasks/issue-3/RELEASE.md on the builder repo.


Roadmap

  • Complete the test suite
  • Streaming uploads/downloads
  • Convenience functions for AsyncJob interactions
  • Toggle for type validation / raw dict input/output

Release notes

1.1.0

Phrase TMS spec refresh (2026-08-18). The generated client now includes the latest documented surface, notably user-profile reads/updates, full due-date scheme CRUD, connector authentication-page and connection-test operations, and job clone/reimport additions. It also incorporates new and expanded response models across the API. TranslationExportEntry.exportWhen is now required by the upstream schema, and the QA setting is named forbiddenStrings.

Existing ConnectorTypeEnum remains available: the generator normalization preserves that public name when the new connector-auth response reuses the same enum values.

Testing and verification. Live tests are now organised by API tag, and new synthetic request/response contract tests exercise every generated operation in the importsettings, analysis, and term_base tags in both sync and async clients (59 operations, 118 exercises). The final verification run passed 900 offline tests and 91 live/destructive tests; 12 live skips document unavailable tenant features or an upstream 501 endpoint.

The live suite now covers 287 of 557 generated operations (51.5%) across 46 of 53 tags. This is measured from the union of direct live-test calls, the read-only sweep, owned-resource settings round trips, and metadata CRUD specs.

1.0.0

This release renames roughly 715 model classes, removes a tag, and makes every generated operation keyword-only. The library itself is the most thoroughly verified it has been: the live suite grew from 8 tests to 257 and now executes 50.6% of the API surface — 275 of 544 operations across 45 of 52 tags — against a real Phrase account, up from 11.2%.

OAuth 2.0 access tokens now work. Phrase declares two security schemes for every operation — ApiToken and OAuth2 — but the client hardcoded the ApiToken prefix, so the second was unreachable: an OAuth token came out as ApiToken <oauth-token>, which does not authenticate. The Authorization scheme is now a property of the client:

pp = Phrappy(token="<OAUTH_ACCESS_TOKEN>", auth_scheme="Bearer")

auth_scheme defaults to "ApiToken", so nothing existing changes, and a token that already carries a recognised scheme is passed through untouched. phrappy does not perform the authorization redirect or the code exchange; obtaining the token remains your application's job. Whether Phrase issues refresh tokens is not documented in the spec or the help centre, so no refresh handling is included yet.

  • A per-call phrase_token= override is now given a scheme too. It was previously sent verbatim, so a bare token passed per call produced an Authorization header with no scheme at all while the same token on the constructor worked.
  • from_creds gains optional keyword-only code (2-factor verification) and user_uid. Omitted values never reach the request body.
  • from_creds now raises PhrappyError when a login response carries no token, instead of returning a client with token = None that failed later with unexplained 401s.
  • Sync make_request raises PhrappyError("Request retries exhausted") after three rate-limited attempts. It previously returned None, so callers hit AttributeError on None.json(). The async client already behaved correctly.

Breaking — models renamed. Models refreshed against the Phrase TMS spec as of 2026-08-04. Phrase re-keyed most component schemas in that release, appending the endpoint's API version, and phrappy's class names follow the vendor's schema names. Roughly 715 model classes were renamed; most gained a version suffix:

0.4.0 1.0.0
JobCreateRequestDto JobCreateRequestDtoV1
CreateProjectV3Dto CreateProjectV3DtoV3
PatchProjectDto PatchProjectDtoV1
AddTargetLangDto RequestBodyForAddingTargetLanguagesToAProject
UserCreateDtoLinguist UserCreateDtoV3Linguist

UidReference, JobPartDeleteReferences, ProjectDtoV2 and other already-versioned names are unchanged. If an import breaks, try the same name with a V1/V2/V3 suffix first.

Breaking — SCIM removed. Phrase removed all six /scim endpoints, so phrappy.tags.scim / SCIMOperations no longer exist. The Project Reference File tag was renamed to Reference File: use client.reference_file instead of client.project_reference_file (same operations).

Breaking — every generated operation is now keyword-only. client.project.get_project(uid) must become client.project.get_project(project_uid=uid).

Parameter order is generated from the spec, and the generator puts required parameters first — so when Phrase makes a request body required, that body moves ahead of the path parameters and silently changes the meaning of a positional call. That is not hypothetical: in this release update_custom_file_type, patch_project, edit_project_v3, set_project_status, clone_project, updates_segmentation_rule and others all reordered, and a call like patch_project(uid, dto) would have bound the uid to the body and serialized the DTO into the URL, producing a confusing runtime error far from the cause.

Keyword-only arguments make that impossible: a stale call now fails immediately with a clear TypeError instead of misbinding. Every call site must be updated once; in exchange, future spec refreshes can never silently rebind your arguments. Phrappy(...) and from_creds(...) are unaffected — those are hand-written and their signatures are stable.

  • New quality_profile tag (evaluate), plus project_template.set_machine_translate_settings_for_project_template and segmentation_rules.replace_segmentation_rule_file.
  • Generator hardening: rendered tag modules and generated models now derive class names from one source of truth, stale tag modules are deleted on regen instead of lingering as broken orphans, and discriminator matching is version-agnostic so a re-keyed schema family no longer breaks the build.

Where the API and its spec disagree. Running half the surface against a real account surfaced a set of behaviours the spec does not describe. None of these are phrappy bugs and none can be fixed in the client — they are documented here because a caller reading the generated signatures would reasonably expect otherwise, and would get a confusing runtime error instead.

Types the API does not honour. Some string/date-time fields arrive as Java Instant objects (handled). NetRateScheme.id is typed string but arrives as an integer. ProviderReference.id is typed string while the user record it refers to types the same value as an integer, so an assignment payload has to stringify it.

Operations that do not exist. POST /transMemories/{uid}/searchContent is published in the spec, generated into the client, and answers 501 NOT_IMPLEMENTED. There is no way to tell from the spec that the operation is absent.

Requests the spec under-specifies. POST /projects/{p}/jobs/{j}/termBases/searchByJob marks its request body optional while the body's query field is required. Codegen honours the spec, so the parameter defaults to None and omitting it is legal by every static measure the client has — and the server answers 500, not 400. Assume a body is required whenever its schema declares a required field, regardless of what requestBody says.

Preconditions invisible from the signature. search_segment_by_job, search_by_job3, wild_card_search_by_job3 and search_terms_by_job_v2 search what the project has assigned, not something named in the request. With nothing assigned they answer 404 Not selected translation memory / TermBaseNotSelected. Separately, create_job returns as soon as the upload is accepted, not when the file has been imported; editing too soon gives 400 JOB_NOT_READY. Poll get_part(...).imported rather than sleeping.

Two reference shapes that look alike. Most references are UidReference ({"uid": ...}), but AddWorkflowStepsDtoV1.workflowSteps and SetTermBaseDtoV1.readTermBases take IdReference ({"id": ...}). Objects carry both, so the wrong one is easy to reach for — a project's term base assignment needs the term base's id while the translation memory assignment beside it needs the memory's uid.

Read and write shapes are not symmetric. Of 17 name-matched GET/PUT settings pairs, only 8 round-trip. Eight share no field at all between the read and write models — including project term bases, translation memories and MT settings — because the read form describes current state while the write form describes an assignment. get_project_template_qa_settings shares the field checks but not its type, and writing the read form back produces 196 validation errors. Read-modify-write is wrong for these nine; construct the write model directly.

A documented fallback that does not hold. UserCreateDtoV3Submitter.automationWidgets is optional, and the spec states that default widgets are assigned when the request names none. On a tenant with no default configured the create is refused outright, and the API publishes no operation that lists automation widgets — the id is only visible in the web UI.

Eventual consistency. An insert into a translation memory is searchable immediately; a clear_trans_memory_v2 is not reflected in search straight away. Poll rather than asserting on the next call.

Worth knowing about webhooks. Phrase does not sign webhook payloads. secretTokenType offers only AUTHORIZATION or X_MEMSOURCE_TOKEN, both of which send your secret as a plain header — so a receiver verifies by comparing a header value, never by computing a digest over the body. The secret is replayable by anyone who observes one delivery, and delivery is not evidence the body is untampered. Treat webhook bodies as untrusted input and re-fetch anything security-relevant through the API. Verified against a live endpoint, not inferred from the docs.

0.4.0

  • Response models are now lenient by default (extra="allow"); request models stay strict. Set PHRAPPY_STRICT=1 before importing to flip response models to extra="forbid" for CI drift detection.
  • Models refreshed against Phrase TMS spec as of 2026-04-21 (12 bi-weekly releases of drift caught up). New language_assets tag module. Manifest grew 401 → 409 operations. ~130 new hoisted enum classes in phrappy.models.
  • Release pipeline rewritten: tag-and-push releases via GitHub Actions, OIDC trusted publishing to PyPI, no host-local copy_to_public.py.
  • Offline fixture-based test harness (295 captured response fixtures) with a drift allowlist that turns PHRAPPY_STRICT=1 into a CI regression gate.

0.3.0

  • Models and operations as of Phrase TMS v25.21 per 28/10 2025. Fixed alias handling.

0.2.0

  • Improved naming of enums that are hoisted from schema inline anonymous declarations.

0.1.0

  • Complete rewrite of build pipeline with fully automated and repeatable builds.
  • Support for polymorph input and output schemas.
  • Slight change in API surface due to schema naming normalization.
  • Added context manager support.
  • Minimal test suite implemented.

License

MIT

Metadata

Release files for phrappy 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for phrappy 1.1.0
File Size Uploaded
phrappy-1.1.0.tar.gz 333.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for phrappy 1.1.0
File Interpreter ABI Platform
phrappy-1.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 757.9 kB

Release files / phrappy-1.1.0.tar.gz

Download URL phrappy-1.1.0.tar.gz
Size 333.4 kB
Tags Source
SHA-256 checksum
How to use checksums
4b89281e953786018f5c947d4294882d279bde1b9e92c966eb7fdfb2f8216ec0
BLAKE2b-256 checksum
How to use checksums
c3b2a2ad8e8fca850d3b8e804ff3bd697431489ddf12d1aeb337d1b10c206bde
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 Aug 19, 2026.

Transparency log

Release files / phrappy-1.1.0-py3-none-any.whl

Download URL phrappy-1.1.0-py3-none-any.whl
Size 424.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
04b74afd6b76211b15b010f6d752502ab2244c6d8d3612ead15594e8cabcfa9d
BLAKE2b-256 checksum
How to use checksums
42a69ea7c825dd49401cf594ff0cdccd47a10aedb46c32cbdbc9314593442ca8
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 Aug 19, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 release files

1.0.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

0.0.1

2 release files

0.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page