onepin 0.2.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.
Files changed (53) hide show
  1. onepin/.fern/metadata.json +14 -0
  2. onepin/CONTRIBUTING.md +125 -0
  3. onepin/__init__.py +48 -0
  4. onepin/_cli/__init__.py +13 -0
  5. onepin/_cli/_ctx.py +28 -0
  6. onepin/_cli/_http.py +126 -0
  7. onepin/_cli/_state.py +7 -0
  8. onepin/_cli/auth/__init__.py +3 -0
  9. onepin/_cli/auth/credentials.py +114 -0
  10. onepin/_cli/auth/resolver.py +76 -0
  11. onepin/_cli/commands/__init__.py +3 -0
  12. onepin/_cli/commands/_registry.py +17 -0
  13. onepin/_cli/commands/auth.py +149 -0
  14. onepin/_cli/commands/templates.py +44 -0
  15. onepin/_cli/commands/uploads.py +52 -0
  16. onepin/_cli/commands/voices.py +21 -0
  17. onepin/_cli/commands/workflows.py +54 -0
  18. onepin/_cli/main.py +80 -0
  19. onepin/_cli/py.typed +0 -0
  20. onepin/_cli/render.py +77 -0
  21. onepin/_default_clients.py +30 -0
  22. onepin/client.py +162 -0
  23. onepin/core/__init__.py +127 -0
  24. onepin/core/api_error.py +23 -0
  25. onepin/core/client_wrapper.py +104 -0
  26. onepin/core/datetime_utils.py +70 -0
  27. onepin/core/file.py +67 -0
  28. onepin/core/force_multipart.py +18 -0
  29. onepin/core/http_client.py +839 -0
  30. onepin/core/http_response.py +59 -0
  31. onepin/core/http_sse/__init__.py +42 -0
  32. onepin/core/http_sse/_api.py +148 -0
  33. onepin/core/http_sse/_decoders.py +61 -0
  34. onepin/core/http_sse/_exceptions.py +7 -0
  35. onepin/core/http_sse/_models.py +17 -0
  36. onepin/core/jsonable_encoder.py +120 -0
  37. onepin/core/logging.py +107 -0
  38. onepin/core/parse_error.py +36 -0
  39. onepin/core/pydantic_utilities.py +646 -0
  40. onepin/core/query_encoder.py +58 -0
  41. onepin/core/remove_none_from_dict.py +11 -0
  42. onepin/core/request_options.py +35 -0
  43. onepin/core/serialization.py +276 -0
  44. onepin/environment.py +15 -0
  45. onepin/py.typed +0 -0
  46. onepin/reference.md +1 -0
  47. onepin/tests/conftest.py +21 -0
  48. onepin/tests/test_aiohttp_autodetect.py +113 -0
  49. onepin-0.2.0.dist-info/METADATA +98 -0
  50. onepin-0.2.0.dist-info/RECORD +53 -0
  51. onepin-0.2.0.dist-info/WHEEL +4 -0
  52. onepin-0.2.0.dist-info/entry_points.txt +2 -0
  53. onepin-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "cliVersion": "5.38.0",
3
+ "generatorName": "fernapi/fern-python-sdk",
4
+ "generatorVersion": "latest",
5
+ "generatorConfig": {
6
+ "client_class_name": "OnePinClient",
7
+ "package_name": "onepin",
8
+ "flat_layout": false
9
+ },
10
+ "originGitCommit": "babfb180c9f10bd3e41e7f774d958819ad52e34e",
11
+ "originGitCommitIsDirty": false,
12
+ "invokedBy": "ci",
13
+ "ciProvider": "github"
14
+ }
onepin/CONTRIBUTING.md ADDED
@@ -0,0 +1,125 @@
1
+ # Contributing
2
+
3
+ Thanks for your interest in contributing to this SDK! This document provides guidelines for contributing to the project.
4
+
5
+ ## Getting Started
6
+
7
+ ### Prerequisites
8
+
9
+ - Python 3.9+
10
+ - pip
11
+ - poetry
12
+
13
+ ### Installation
14
+
15
+ Install the project dependencies:
16
+
17
+ ```bash
18
+ poetry install
19
+ ```
20
+
21
+ ### Building
22
+
23
+ Build the project:
24
+
25
+ ```bash
26
+ poetry build
27
+ ```
28
+
29
+ ### Testing
30
+
31
+ Run the test suite:
32
+
33
+ ```bash
34
+ poetry run pytest
35
+ ```
36
+
37
+ ### Linting and Formatting
38
+
39
+ Check code style:
40
+
41
+ ```bash
42
+ poetry run ruff check .
43
+ poetry run ruff format .
44
+ ```
45
+
46
+ ### Type Checking
47
+
48
+ Run the type checker:
49
+
50
+ ```bash
51
+ poetry run mypy .
52
+ ```
53
+
54
+ ## About Generated Code
55
+
56
+ **Important**: Most files in this SDK are automatically generated by [Fern](https://buildwithfern.com) from the API definition. Direct modifications to generated files will be overwritten the next time the SDK is generated.
57
+
58
+ ### Generated Files
59
+
60
+ The following directories contain generated code:
61
+ - `src/` - API client classes and types
62
+ - Most Python files in the project
63
+
64
+ ### How to Customize
65
+
66
+ If you need to customize the SDK, you have two options:
67
+
68
+ #### Option 1: Use `.fernignore`
69
+
70
+ For custom code that should persist across SDK regenerations:
71
+
72
+ 1. Create a `.fernignore` file in the project root
73
+ 2. Add file patterns for files you want to preserve (similar to `.gitignore` syntax)
74
+ 3. Add your custom code to those files
75
+
76
+ Files listed in `.fernignore` will not be overwritten when the SDK is regenerated.
77
+
78
+ For more information, see the [Fern documentation on custom code](https://buildwithfern.com/learn/sdks/overview/custom-code).
79
+
80
+ #### Option 2: Contribute to the Generator
81
+
82
+ If you want to change how code is generated for all users of this SDK:
83
+
84
+ 1. The Python SDK generator lives in the [Fern repository](https://github.com/fern-api/fern)
85
+ 2. Generator code is located at `generators/python-v2/`
86
+ 3. Follow the [Fern contributing guidelines](https://github.com/fern-api/fern/blob/main/CONTRIBUTING.md)
87
+ 4. Submit a pull request with your changes to the generator
88
+
89
+ This approach is best for:
90
+ - Bug fixes in generated code
91
+ - New features that would benefit all users
92
+ - Improvements to code generation patterns
93
+
94
+ ## Making Changes
95
+
96
+ ### Workflow
97
+
98
+ 1. Create a new branch for your changes
99
+ 2. Make your modifications
100
+ 3. Run tests to ensure nothing breaks: `poetry run pytest`
101
+ 4. Run linting and formatting: `poetry run ruff check .` and `poetry run ruff format .`
102
+ 5. Run type checking: `poetry run mypy .`
103
+ 6. Build the project: `poetry build`
104
+ 7. Commit your changes with a clear commit message
105
+ 8. Push your branch and create a pull request
106
+
107
+ ### Commit Messages
108
+
109
+ Write clear, descriptive commit messages that explain what changed and why.
110
+
111
+ ### Code Style
112
+
113
+ This project uses automated code formatting and linting. Run `poetry run ruff format .` and `poetry run ruff check .` before committing to ensure your code meets the project's style guidelines.
114
+
115
+ ## Questions or Issues?
116
+
117
+ If you have questions or run into issues:
118
+
119
+ 1. Check the [Fern documentation](https://buildwithfern.com)
120
+ 2. Search existing [GitHub issues](https://github.com/fern-api/fern/issues)
121
+ 3. Open a new issue if your question hasn't been addressed
122
+
123
+ ## License
124
+
125
+ By contributing to this project, you agree that your contributions will be licensed under the same license as the project.
onepin/__init__.py ADDED
@@ -0,0 +1,48 @@
1
+ # This file was auto-generated by Fern from our API Definition.
2
+
3
+ # isort: skip_file
4
+
5
+ import typing
6
+ from importlib import import_module
7
+
8
+ if typing.TYPE_CHECKING:
9
+ from ._default_clients import DefaultAioHttpClient, DefaultAsyncHttpxClient
10
+ from .client import AsyncOnePinClient, OnePinClient
11
+ from .environment import OnePinClientEnvironment
12
+ _dynamic_imports: typing.Dict[str, str] = {
13
+ "AsyncOnePinClient": ".client",
14
+ "DefaultAioHttpClient": "._default_clients",
15
+ "DefaultAsyncHttpxClient": "._default_clients",
16
+ "OnePinClient": ".client",
17
+ "OnePinClientEnvironment": ".environment",
18
+ }
19
+
20
+
21
+ def __getattr__(attr_name: str) -> typing.Any:
22
+ module_name = _dynamic_imports.get(attr_name)
23
+ if module_name is None:
24
+ raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}")
25
+ try:
26
+ module = import_module(module_name, __package__)
27
+ if module_name == f".{attr_name}":
28
+ return module
29
+ else:
30
+ return getattr(module, attr_name)
31
+ except ImportError as e:
32
+ raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e
33
+ except AttributeError as e:
34
+ raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e
35
+
36
+
37
+ def __dir__():
38
+ lazy_attrs = list(_dynamic_imports.keys())
39
+ return sorted(lazy_attrs)
40
+
41
+
42
+ __all__ = [
43
+ "AsyncOnePinClient",
44
+ "DefaultAioHttpClient",
45
+ "DefaultAsyncHttpxClient",
46
+ "OnePinClient",
47
+ "OnePinClientEnvironment",
48
+ ]
@@ -0,0 +1,13 @@
1
+ """OnePin CLI -- hand-rolled Typer CLI atop the Fern-generated SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import PackageNotFoundError
6
+ from importlib.metadata import version as _pkg_version
7
+
8
+ try:
9
+ __version__ = _pkg_version("onepin")
10
+ except PackageNotFoundError: # editable install pre-build
11
+ __version__ = "0.0.0+local"
12
+
13
+ __all__ = ["__version__"]
onepin/_cli/_ctx.py ADDED
@@ -0,0 +1,28 @@
1
+ """Build OnePinClient from resolved credentials.
2
+
3
+ Pending Fern SDK regen -- raises NotImplementedError until client.py exists.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from onepin._cli.auth.resolver import ResolvedCredentials
11
+
12
+
13
+ def build_client(creds: ResolvedCredentials) -> Any:
14
+ """Build and return an OnePinClient from resolved credentials.
15
+
16
+ Args:
17
+ creds: Resolved credentials from the priority chain.
18
+
19
+ Returns:
20
+ OnePinClient instance.
21
+
22
+ Raises:
23
+ NotImplementedError: Until the first Fern SDK regen lands.
24
+ """
25
+ raise NotImplementedError(
26
+ "OnePinClient is not yet available -- Fern SDK regen has not run yet. "
27
+ "See podonos/onepin-sdks for the Fern configuration."
28
+ )
onepin/_cli/_http.py ADDED
@@ -0,0 +1,126 @@
1
+ """Shared httpx helper for CLI auth calls.
2
+
3
+ Provides a typed exception hierarchy and a single _call_whoami() function
4
+ used by login, logout, and whoami commands.
5
+
6
+ No Fern SDK dependency -- direct httpx calls only.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from typing import Any, Dict, Optional
13
+
14
+ import httpx
15
+
16
+ from onepin._cli import __version__
17
+
18
+
19
+ class OnePinHTTPError(Exception):
20
+ """Base error for all HTTP-layer failures.
21
+
22
+ Attributes:
23
+ status_code: HTTP status code, or None for network-level errors.
24
+ error_code: Machine-readable error code from the response envelope.
25
+ message: Human-readable message.
26
+ request_id: Request-ID from the response meta envelope, if available.
27
+ response_body: Raw response body string, if available.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ message: str,
33
+ *,
34
+ status_code: Optional[int] = None,
35
+ error_code: str = "HTTP_ERROR",
36
+ request_id: Optional[str] = None,
37
+ response_body: Optional[str] = None,
38
+ ) -> None:
39
+ super().__init__(message)
40
+ self.status_code = status_code
41
+ self.error_code = error_code
42
+ self.message = message
43
+ self.request_id = request_id
44
+ self.response_body = response_body
45
+
46
+
47
+ class OnePinAuthError(OnePinHTTPError):
48
+ """Raised for 401 / 403 responses."""
49
+
50
+
51
+ class OnePinNetworkError(OnePinHTTPError):
52
+ """Raised for connection / timeout failures (no status code)."""
53
+
54
+
55
+ def _user_agent() -> str:
56
+ major = sys.version_info.major
57
+ minor = sys.version_info.minor
58
+ return f"onepin-python/{__version__} python/{major}.{minor}"
59
+
60
+
61
+ def _call_whoami(key: str, base_url: str, timeout: float = 10.0) -> Dict[str, Any]:
62
+ """Call GET /api/v1/auth/whoami and return the ``data`` field.
63
+
64
+ Args:
65
+ key: API key to authenticate with.
66
+ base_url: Base URL of the OnePin API (no trailing slash).
67
+ timeout: Request timeout in seconds.
68
+
69
+ Returns:
70
+ Parsed ``data`` dict from the response envelope.
71
+
72
+ Raises:
73
+ OnePinAuthError: On 401 or 403.
74
+ OnePinNetworkError: On connection or timeout failure.
75
+ OnePinHTTPError: On any other non-2xx response.
76
+ """
77
+ url = f"{base_url.rstrip('/')}/api/v1/auth/whoami"
78
+ headers = {
79
+ "Authorization": f"Bearer {key}",
80
+ "User-Agent": _user_agent(),
81
+ }
82
+ try:
83
+ with httpx.Client(timeout=timeout) as client:
84
+ response = client.get(url, headers=headers)
85
+ except (httpx.ConnectError, httpx.TimeoutException) as exc:
86
+ raise OnePinNetworkError(
87
+ f"Could not reach {base_url}. Pass --verbose for details.",
88
+ error_code="NETWORK_ERROR",
89
+ ) from exc
90
+
91
+ # Try to extract envelope fields for richer errors
92
+ request_id: Optional[str] = None
93
+ error_code = "HTTP_ERROR"
94
+ error_message = response.reason_phrase or "Request failed"
95
+ body_text = response.text
96
+
97
+ if response.status_code != 200:
98
+ try:
99
+ payload = response.json()
100
+ meta = payload.get("meta", {})
101
+ request_id = meta.get("request_id")
102
+ error_obj = payload.get("error", {})
103
+ error_code = error_obj.get("code", error_code)
104
+ error_message = error_obj.get("message", error_message)
105
+ except Exception: # noqa: BLE001
106
+ pass
107
+
108
+ if response.status_code in (401, 403):
109
+ raise OnePinAuthError(
110
+ error_message,
111
+ status_code=response.status_code,
112
+ error_code="INVALID_API_KEY",
113
+ request_id=request_id,
114
+ response_body=body_text,
115
+ )
116
+
117
+ raise OnePinHTTPError(
118
+ error_message,
119
+ status_code=response.status_code,
120
+ error_code=error_code,
121
+ request_id=request_id,
122
+ response_body=body_text,
123
+ )
124
+
125
+ payload = response.json()
126
+ return payload["data"]
onepin/_cli/_state.py ADDED
@@ -0,0 +1,7 @@
1
+ """Process-local CLI option state shared between root callback and commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ root_options: dict[str, Any] = {}
@@ -0,0 +1,3 @@
1
+ """CLI auth helpers: credential resolution and file I/O."""
2
+
3
+ from __future__ import annotations
@@ -0,0 +1,114 @@
1
+ """Read/write ~/.onepin/credentials TOML file with mode 0600.
2
+
3
+ Uses ``tomllib`` (stdlib 3.11+) or ``tomli`` (backport for 3.10).
4
+ Writes are hand-formatted TOML -- avoids the ``tomli_w`` dependency.
5
+
6
+ Malformed TOML on read raises SystemExit(1) -- fail fast, no silent skip.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import stat
13
+ import sys
14
+ from pathlib import Path, PosixPath, WindowsPath
15
+ from typing import Any, Dict, Optional
16
+
17
+
18
+ def _home_path() -> Path:
19
+ """Resolve the CLI home directory in a testable, cross-platform way."""
20
+ path_cls = WindowsPath if sys.platform == "win32" else PosixPath
21
+
22
+ home = os.environ.get("HOME")
23
+ if home:
24
+ return path_cls(home)
25
+
26
+ if sys.platform == "win32":
27
+ user_profile = os.environ.get("USERPROFILE")
28
+ if user_profile:
29
+ return path_cls(user_profile)
30
+
31
+ home_drive = os.environ.get("HOMEDRIVE")
32
+ home_path = os.environ.get("HOMEPATH")
33
+ if home_drive and home_path:
34
+ return path_cls(home_drive + home_path)
35
+
36
+ return Path.home()
37
+
38
+
39
+ def _credentials_path() -> Path:
40
+ return _home_path() / ".onepin" / "credentials"
41
+
42
+
43
+ def _load_toml(path: Path) -> Dict[str, Any]:
44
+ """Parse TOML from path. Hard-fails on parse error."""
45
+ try:
46
+ if sys.version_info >= (3, 11):
47
+ import tomllib
48
+
49
+ with open(path, "rb") as f:
50
+ return tomllib.load(f)
51
+ else:
52
+ import tomli # type: ignore[import-not-found]
53
+
54
+ with open(path, "rb") as f:
55
+ return tomli.load(f)
56
+ except Exception as exc: # noqa: BLE001
57
+ sys.stderr.write(f"[ERROR] Malformed credentials file at {path}: {exc}\n")
58
+ sys.exit(1)
59
+
60
+
61
+ def read_credentials() -> Optional[Dict[str, Any]]:
62
+ """Read credentials file. Returns None if file does not exist.
63
+
64
+ Raises:
65
+ SystemExit(1): If the file exists but cannot be parsed as TOML.
66
+ """
67
+ path = _credentials_path()
68
+ if not path.exists():
69
+ return None
70
+ return _load_toml(path)
71
+
72
+
73
+ def write_credentials(api_key: str, base_url: str, profile: str = "default") -> Path:
74
+ """Write credentials to ~/.onepin/credentials with mode 0600.
75
+
76
+ Creates ~/.onepin/ with mode 0700 if it does not exist.
77
+
78
+ Args:
79
+ api_key: The API key to store.
80
+ base_url: The base URL to store.
81
+ profile: Profile name (default: "default").
82
+
83
+ Returns:
84
+ Path to the written credentials file.
85
+ """
86
+ path = _credentials_path()
87
+ parent = path.parent
88
+
89
+ # Create parent directory with mode 0700
90
+ parent.mkdir(mode=0o700, parents=True, exist_ok=True)
91
+
92
+ content = f'[{profile}]\napi_key = "{api_key}"\nbase_url = "{base_url}"\n'
93
+
94
+ # Write atomically: write to temp file, then rename
95
+ tmp_path = path.with_suffix(".tmp")
96
+ try:
97
+ tmp_path.write_text(content, encoding="utf-8")
98
+ if os.name != "nt":
99
+ os.chmod(tmp_path, stat.S_IRUSR | stat.S_IWUSR) # 0600
100
+ tmp_path.replace(path)
101
+ except Exception:
102
+ tmp_path.unlink(missing_ok=True)
103
+ raise
104
+
105
+ return path
106
+
107
+
108
+ def delete_credentials() -> bool:
109
+ """Delete credentials file. Returns True if deleted, False if not found."""
110
+ path = _credentials_path()
111
+ if path.exists():
112
+ path.unlink()
113
+ return True
114
+ return False
@@ -0,0 +1,76 @@
1
+ """Credential resolution: flag > env > file > None.
2
+
3
+ Priority chain (highest wins):
4
+ 1. Explicit --api-key flag (passed as ``flag_api_key``)
5
+ 2. ``ONEPIN_API_KEY`` environment variable
6
+ 3. ``~/.onepin/credentials`` TOML file, ``[default]`` profile
7
+ 4. None (caller decides whether to abort)
8
+
9
+ Malformed TOML file raises hard error -- no silent fallback per project policy.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ from dataclasses import dataclass
16
+ from typing import Literal, Optional
17
+
18
+ from onepin._cli.auth.credentials import read_credentials
19
+
20
+
21
+ @dataclass
22
+ class ResolvedCredentials:
23
+ api_key: Optional[str]
24
+ base_url: Optional[str]
25
+ source: Literal["flag", "env", "file", "default"]
26
+
27
+
28
+ def resolve_credentials(
29
+ flag_api_key: Optional[str] = None,
30
+ flag_base_url: Optional[str] = None,
31
+ ) -> ResolvedCredentials:
32
+ """Resolve credentials from the priority chain.
33
+
34
+ Args:
35
+ flag_api_key: Value of --api-key CLI flag (None if not provided).
36
+ flag_base_url: Value of --base-url CLI flag (None if not provided).
37
+
38
+ Returns:
39
+ ResolvedCredentials with source indicating where the key came from.
40
+
41
+ Raises:
42
+ SystemExit: If the credentials file exists but is malformed TOML.
43
+ """
44
+ # 1. Explicit flag
45
+ if flag_api_key is not None:
46
+ return ResolvedCredentials(
47
+ api_key=flag_api_key,
48
+ base_url=flag_base_url,
49
+ source="flag",
50
+ )
51
+
52
+ # 2. Environment variable
53
+ env_key = os.environ.get("ONEPIN_API_KEY")
54
+ if env_key:
55
+ env_url = os.environ.get("ONEPIN_BASE_URL", flag_base_url)
56
+ return ResolvedCredentials(
57
+ api_key=env_key,
58
+ base_url=env_url,
59
+ source="env",
60
+ )
61
+
62
+ # 3. Credentials file (~/.onepin/credentials)
63
+ file_data = read_credentials() # raises hard on malformed TOML
64
+ if file_data is not None:
65
+ profile = file_data.get("default", {})
66
+ file_key = profile.get("api_key")
67
+ file_url = flag_base_url or profile.get("base_url")
68
+ if file_key:
69
+ return ResolvedCredentials(
70
+ api_key=file_key,
71
+ base_url=file_url,
72
+ source="file",
73
+ )
74
+
75
+ # 4. Nothing found
76
+ return ResolvedCredentials(api_key=None, base_url=flag_base_url, source="default")
@@ -0,0 +1,3 @@
1
+ """CLI command modules."""
2
+
3
+ from __future__ import annotations
@@ -0,0 +1,17 @@
1
+ """Wire subcommands into the root Typer app."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from onepin._cli.commands import auth, templates, uploads, voices, workflows
8
+
9
+
10
+ def register(app: typer.Typer) -> None:
11
+ app.command(name="login")(auth.login)
12
+ app.command(name="logout")(auth.logout)
13
+ app.command(name="whoami")(auth.whoami)
14
+ app.add_typer(workflows.app, name="workflows")
15
+ app.add_typer(voices.app, name="voices")
16
+ app.add_typer(templates.app, name="templates")
17
+ app.add_typer(uploads.app, name="uploads")