lingva-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lingva/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ from lingva._client import LingvaClient, LingvaSchemaError
2
+ from lingva._delivery import (
3
+ DEFAULT_REMOTE_BUNDLE_PATH_TEMPLATE,
4
+ LingvaDeliveryTarget,
5
+ load_bundle_from_file,
6
+ )
7
+
8
+ __all__ = [
9
+ "DEFAULT_REMOTE_BUNDLE_PATH_TEMPLATE",
10
+ "LingvaClient",
11
+ "LingvaDeliveryTarget",
12
+ "LingvaSchemaError",
13
+ "load_bundle_from_file",
14
+ ]
lingva/_client.py ADDED
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from lingva._delivery import (
8
+ LingvaDeliveryTarget,
9
+ load_bundle_from_file,
10
+ load_bundle_from_url_async,
11
+ load_bundle_from_url_sync,
12
+ )
13
+
14
+ _SCHEMA_VERSION = 1
15
+ _PLACEHOLDER_RE = re.compile(r"\{(\w+)\}")
16
+
17
+
18
+ def _interpolate(template: str, variables: dict[str, str]) -> str:
19
+ """Replace {varname} tokens; unknown tokens pass through unchanged."""
20
+ def replace(m: re.Match[str]) -> str:
21
+ return variables.get(m.group(1), m.group(0))
22
+ return _PLACEHOLDER_RE.sub(replace, template)
23
+
24
+
25
+ class LingvaSchemaError(ValueError):
26
+ """Raised when a bundle fails schema validation."""
27
+
28
+
29
+ class LingvaClient:
30
+ """
31
+ Reads a Lingva locale bundle and provides key lookup with fallback and
32
+ variable interpolation.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ bundle: dict[str, Any],
38
+ fallback_bundle: dict[str, Any] | None = None,
39
+ ) -> None:
40
+ if bundle.get("schemaVersion") != _SCHEMA_VERSION:
41
+ raise LingvaSchemaError(
42
+ f"Unsupported bundle schemaVersion: {bundle.get('schemaVersion')!r}"
43
+ )
44
+ self._bundle = bundle
45
+ self._messages: dict[str, str] = bundle.get("messages") or {}
46
+ self._fallback_messages: dict[str, str] = (
47
+ fallback_bundle.get("messages") or {} if fallback_bundle else {}
48
+ )
49
+ self._fallback_keys: set[str] = set(bundle.get("fallbackKeys") or [])
50
+ self._locale: str = bundle.get("locale") or bundle.get("defaultLocale") or "en"
51
+ self._fallback_locale: str | None = bundle.get("fallbackLocale")
52
+ self._last_valid: dict[str, Any] = bundle
53
+
54
+ # ------------------------------------------------------------------
55
+ # Factory methods
56
+ # ------------------------------------------------------------------
57
+
58
+ @classmethod
59
+ def from_bundle(cls, bundle: dict[str, Any]) -> "LingvaClient":
60
+ """Create a client from an already-parsed bundle dict."""
61
+ return cls(bundle)
62
+
63
+ @classmethod
64
+ def from_file(cls, path: str | Path, *, fallback_path: str | Path | None = None) -> "LingvaClient":
65
+ """Load a bundle from the local filesystem."""
66
+ bundle = load_bundle_from_file(path)
67
+ fallback = load_bundle_from_file(fallback_path) if fallback_path else None
68
+ return cls(bundle, fallback)
69
+
70
+ @classmethod
71
+ def from_url_sync(
72
+ cls,
73
+ target: LingvaDeliveryTarget,
74
+ locale: str,
75
+ *,
76
+ fallback_locale: str | None = None,
77
+ last_valid: dict[str, Any] | None = None,
78
+ ) -> "LingvaClient":
79
+ """Fetch a bundle synchronously; return a client over `last_valid` on error."""
80
+ try:
81
+ bundle = load_bundle_from_url_sync(target.resolve(locale))
82
+ except Exception:
83
+ if last_valid is not None:
84
+ return cls(last_valid)
85
+ raise
86
+ fallback = None
87
+ if fallback_locale and fallback_locale != locale:
88
+ try:
89
+ fallback = load_bundle_from_url_sync(target.resolve(fallback_locale))
90
+ except Exception:
91
+ pass
92
+ return cls(bundle, fallback)
93
+
94
+ @classmethod
95
+ async def from_url(
96
+ cls,
97
+ target: LingvaDeliveryTarget,
98
+ locale: str,
99
+ *,
100
+ fallback_locale: str | None = None,
101
+ last_valid: dict[str, Any] | None = None,
102
+ ) -> "LingvaClient":
103
+ """Fetch a bundle asynchronously; return a client over `last_valid` on error."""
104
+ try:
105
+ bundle = await load_bundle_from_url_async(target.resolve(locale))
106
+ except Exception:
107
+ if last_valid is not None:
108
+ return cls(last_valid)
109
+ raise
110
+ fallback = None
111
+ if fallback_locale and fallback_locale != locale:
112
+ try:
113
+ fallback = await load_bundle_from_url_async(target.resolve(fallback_locale))
114
+ except Exception:
115
+ pass
116
+ return cls(bundle, fallback)
117
+
118
+ # ------------------------------------------------------------------
119
+ # Public API
120
+ # ------------------------------------------------------------------
121
+
122
+ @property
123
+ def locale(self) -> str:
124
+ return self._locale
125
+
126
+ @property
127
+ def fallback_locale(self) -> str | None:
128
+ return self._fallback_locale
129
+
130
+ def has(self, key: str) -> bool:
131
+ """Return True if the key exists in the primary or fallback messages."""
132
+ return key in self._messages or key in self._fallback_messages
133
+
134
+ def t(self, key: str, **variables: str) -> str:
135
+ """
136
+ Translate `key` with optional variable substitution.
137
+
138
+ Falls back to `fallback_bundle` when the key is missing from the
139
+ primary bundle or is listed in `fallbackKeys`.
140
+ """
141
+ if key in self._fallback_keys or key not in self._messages:
142
+ template = self._fallback_messages.get(key) or self._messages.get(key)
143
+ else:
144
+ template = self._messages.get(key)
145
+
146
+ if template is None:
147
+ return key
148
+
149
+ return _interpolate(template, variables) if variables else template
150
+
151
+ def bundle(self) -> dict[str, Any]:
152
+ """Return the raw bundle dict (useful for last-valid caching)."""
153
+ return self._bundle
lingva/_delivery.py ADDED
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import urllib.request
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ DEFAULT_REMOTE_BUNDLE_PATH_TEMPLATE = "/bundles/{projectId}/{environment}/latest/{locale}.bundle.json"
10
+
11
+ _PLACEHOLDERS = {"projectId", "environment", "locale"}
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class LingvaDeliveryTarget:
16
+ """Resolves a Lingva CDN delivery URL for a given locale."""
17
+ base_url: str
18
+ uri_template: str = DEFAULT_REMOTE_BUNDLE_PATH_TEMPLATE
19
+ project_id: str = ""
20
+ environment: str = "production"
21
+
22
+ def resolve(self, locale: str) -> str:
23
+ path = (
24
+ self.uri_template
25
+ .replace("{projectId}", self.project_id)
26
+ .replace("{environment}", self.environment)
27
+ .replace("{locale}", locale)
28
+ )
29
+ return self.base_url.rstrip("/") + path
30
+
31
+
32
+ def load_bundle_from_file(path: str | Path) -> dict[str, Any]:
33
+ """Read and parse a locale bundle JSON file."""
34
+ return json.loads(Path(path).read_text(encoding="utf-8"))
35
+
36
+
37
+ def load_bundle_from_url_sync(url: str) -> dict[str, Any]:
38
+ """Fetch a locale bundle over HTTP (synchronous, no extra deps)."""
39
+ with urllib.request.urlopen(url) as resp: # noqa: S310
40
+ return json.loads(resp.read().decode("utf-8"))
41
+
42
+
43
+ async def load_bundle_from_url_async(url: str) -> dict[str, Any]:
44
+ """Fetch a locale bundle over HTTP using httpx (async)."""
45
+ try:
46
+ import httpx
47
+ except ImportError as exc:
48
+ raise ImportError(
49
+ "Install 'lingva[http]' (httpx) for async HTTP delivery."
50
+ ) from exc
51
+ async with httpx.AsyncClient() as client:
52
+ response = await client.get(url)
53
+ response.raise_for_status()
54
+ return response.json()
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.5
2
+ Name: lingva-sdk
3
+ Version: 0.1.0
4
+ Summary: Python client for the Lingva localization platform
5
+ License: SEE LICENSE IN ../../LICENSE.md
6
+ Requires-Python: >=3.11
7
+ Provides-Extra: dev
8
+ Requires-Dist: httpx>=0.27; extra == 'dev'
9
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
10
+ Requires-Dist: pytest>=8; extra == 'dev'
11
+ Provides-Extra: http
12
+ Requires-Dist: httpx>=0.27; extra == 'http'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # lingva — Python SDK
16
+
17
+ Python client for the [Lingva](https://lingva.dev) localization platform.
18
+
19
+ **Requires Python 3.11+.**
20
+
21
+ > This is a preview package. It follows the same conformance contract as the Swift, Kotlin, and Flutter SDKs but is not yet published to PyPI.
22
+
23
+ ---
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ # From the monorepo root (editable install)
29
+ pip install -e sdks/python
30
+
31
+ # With async HTTP support (httpx)
32
+ pip install -e "sdks/python[http]"
33
+ ```
34
+
35
+ Once published:
36
+
37
+ ```bash
38
+ pip install lingva-sdk
39
+ pip install "lingva-sdk[http]" # adds httpx for async delivery
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Quickstart
45
+
46
+ ### Load from a local bundle file
47
+
48
+ ```python
49
+ from lingva import LingvaClient
50
+
51
+ client = LingvaClient.from_file(".lingva/translations/en.bundle.json")
52
+
53
+ print(client.t("actions.continue")) # Continue
54
+ print(client.t("welcome.title", name="Alice")) # Welcome, Alice.
55
+ ```
56
+
57
+ ### Load from CDN (async)
58
+
59
+ ```python
60
+ import asyncio
61
+ from lingva import LingvaClient, LingvaDeliveryTarget
62
+
63
+ target = LingvaDeliveryTarget(
64
+ base_url="https://api.lingva.dev",
65
+ project_id="my-project",
66
+ environment="production",
67
+ )
68
+
69
+ async def main():
70
+ client = await LingvaClient.from_url(target, locale="fr", fallback_locale="en")
71
+ print(client.t("welcome.title", name="Alice"))
72
+
73
+ asyncio.run(main())
74
+ ```
75
+
76
+ ### Load from CDN (synchronous)
77
+
78
+ ```python
79
+ from lingva import LingvaClient, LingvaDeliveryTarget
80
+
81
+ target = LingvaDeliveryTarget(
82
+ base_url="https://api.lingva.dev",
83
+ project_id="my-project",
84
+ environment="production",
85
+ )
86
+
87
+ client = LingvaClient.from_url_sync(target, locale="fr", fallback_locale="en")
88
+ print(client.t("welcome.title", name="Alice"))
89
+ ```
90
+
91
+ ---
92
+
93
+ ## API
94
+
95
+ ### `LingvaClient`
96
+
97
+ | Method / Property | Description |
98
+ |---|---|
99
+ | `LingvaClient.from_bundle(bundle)` | Create from a parsed dict |
100
+ | `LingvaClient.from_file(path, *, fallback_path=None)` | Load from filesystem |
101
+ | `LingvaClient.from_url_sync(target, locale, ...)` | Synchronous HTTP fetch |
102
+ | `await LingvaClient.from_url(target, locale, ...)` | Async HTTP fetch (requires `lingva[http]`) |
103
+ | `.t(key, **variables)` | Translate a key with optional `{varname}` substitution |
104
+ | `.has(key)` | Check if a key exists |
105
+ | `.locale` | Active locale string |
106
+ | `.fallback_locale` | Fallback locale string or `None` |
107
+ | `.bundle()` | Raw bundle dict (for last-valid caching) |
108
+
109
+ ### `LingvaDeliveryTarget`
110
+
111
+ ```python
112
+ LingvaDeliveryTarget(
113
+ base_url="https://api.lingva.dev",
114
+ project_id="my-project",
115
+ environment="production", # default
116
+ uri_template="...", # optional custom path template
117
+ )
118
+ target.resolve("fr")
119
+ # -> "https://api.lingva.dev/bundles/my-project/production/latest/fr.bundle.json"
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Bundle schema
125
+
126
+ The SDK reads schemaVersion 1 bundles. Variables use `{varname}` syntax. Unknown variables pass through unchanged.
127
+
128
+ ```json
129
+ {
130
+ "schemaVersion": 1,
131
+ "locale": "en",
132
+ "fallbackLocale": "en",
133
+ "fallbackKeys": [],
134
+ "messages": {
135
+ "actions.continue": "Continue",
136
+ "welcome.title": "Welcome, {name}."
137
+ }
138
+ }
139
+ ```
@@ -0,0 +1,6 @@
1
+ lingva/__init__.py,sha256=IofiScxexQBorrgU3gdHVzPH7ab4jhXRuaiBAAOo9XA,346
2
+ lingva/_client.py,sha256=baxKPjEu3_5_TuyzuReVmSFgrwCve8A36j0Mn9xKC3Q,5292
3
+ lingva/_delivery.py,sha256=IR2Q4xs0H7qMKsQ9VMhUy6HdX9yN-HHnD0Tf3kzsisc,1765
4
+ lingva_sdk-0.1.0.dist-info/METADATA,sha256=wafaa5HCPgbwRaMshgliz7ER404RbKO35bMKWDJGM3s,3460
5
+ lingva_sdk-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
6
+ lingva_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any