phrappy
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
dictfor 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
PhrappyandAsyncPhrappyunder the same tag-based namespaces. - Built on
httpxwith 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 raisingValidationError. 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=1in the environment before importingphrappyto flip response models toextra="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 viaPhrappy(base_url=...)orAsyncPhrappy(base_url=...). Profiles in the US data centre needhttps://us.cloud.memsource.com/web. - Pass
timeout=(seconds) to the client constructor. Per-request timeouts are also supported onmake_requestif you wrap custom calls. auth_scheme=sets theAuthorizationscheme 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-callphrase_token=override.
Testing
The test suite has three layers:
- Offline fixture tests — captured Phrase TMS response bodies replayed through the generated Pydantic response models. Runs by default, no network, fast.
- Drift detection — same fixtures re-run under
PHRAPPY_STRICT=1to surface upstream schema divergence (unexpected fields, etc.) that lenient mode would silently tolerate. - 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
ValidationErrorin 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_TOKENor (PHRAPPY_USERandPHRAPPY_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:
- Bump
__version__insrc/phrappy/_meta.py. - Add a
### X.Y.Zsection to the## Release notesbelow. - Commit to
main, runpytest -m "not live and not destructive"locally. 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 anAuthorizationheader with no scheme at all while the same token on the constructor worked. from_credsgains optional keyword-onlycode(2-factor verification) anduser_uid. Omitted values never reach the request body.from_credsnow raisesPhrappyErrorwhen a login response carries no token, instead of returning a client withtoken = Nonethat failed later with unexplained 401s.- Sync
make_requestraisesPhrappyError("Request retries exhausted")after three rate-limited attempts. It previously returnedNone, so callers hitAttributeErroronNone.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_profiletag (evaluate), plusproject_template.set_machine_translate_settings_for_project_templateandsegmentation_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. SetPHRAPPY_STRICT=1before importing to flip response models toextra="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_assetstag module. Manifest grew 401 → 409 operations. ~130 new hoisted enum classes inphrappy.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=1into 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)
| File | Size | Uploaded | |
|---|---|---|---|
| phrappy-1.1.0.tar.gz | 333.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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