Metadata-Version: 2.4
Name: phrappy
Version: 0.4.0
Summary: Typed client for Phrase TMS (Memsource) generated from OpenAPI.
Project-URL: Homepage, https://github.com/kuhnemann/phrappy
Author: Henrik Kühnemann
License: MIT
Keywords: api,client,memsource,phrase,tms
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.28.1
Requires-Dist: pydantic>=2
Provides-Extra: codegen
Requires-Dist: datamodel-code-generator>=0.26; extra == 'codegen'
Requires-Dist: diff-match-patch>=20241021; extra == 'codegen'
Requires-Dist: jinja2>=3.1; extra == 'codegen'
Requires-Dist: pyyaml>=6; extra == 'codegen'
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
Requires-Dist: pytest-recording>=0.13.1; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: python-dotenv>=1.1.0; extra == 'dev'
Requires-Dist: ruff>=0.12.11; extra == 'dev'
Description-Content-Type: text/markdown

# phrappy 
[![PyPI Downloads](https://static.pepy.tech/personalized-badge/phrappy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/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](https://developers.phrase.com/en/api/tms/latest/introduction)

---

## Installation

```bash
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.
```python
from phrappy import Phrappy
from phrappy.models import LoginDto

pp = Phrappy()
login_response = pp.authentication.login(LoginDto(
    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:
```python
from phrappy import Phrappy


pp = Phrappy()
login_response = pp.authentication.login({
    "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. 
```python
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()
```

Using a context manager closes the underlying HTTP client automatically:
```python
from phrappy import Phrappy

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


### 3) Async usage
```python
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)
```python
from pathlib import Path
from phrappy import Phrappy, cdh_generator
from phrappy.models import CreateProjectV3Dto, JobCreateRequestDto

with Phrappy(token="<YOUR_TOKEN>") as pp:
    proj = pp.project.create_project_v3(CreateProjectV3Dto(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=JobCreateRequestDto(targetLangs=proj.targetLangs),
    )
    print([j.uid for j in jobs.jobs or []])
```

### List your assigned projects
```python
me = pp.authentication.who_am_i()
page = pp.project.list_assigned_projects(me.user.uid, target_lang=["sv"])  # returns a 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.
- 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=...)`.
- Pass `timeout=` (seconds) to the client constructor. Per-request timeouts are also supported on `make_request` if you wrap custom calls.

---

## 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.

```bash
# 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
### 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

