p0aiapi 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.
p0aiapi/__init__.py ADDED
@@ -0,0 +1,46 @@
1
+ """Public interface for p0aiapi."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ from .catalog import load_catalog
6
+ from .client import AsyncClient, Client, P0ResponseError
7
+ from .exceptions import (
8
+ APIError,
9
+ AuthenticationError,
10
+ NotFoundError,
11
+ P0Error,
12
+ RateLimitError,
13
+ ServerError,
14
+ TaskFailedError,
15
+ TaskTimeoutError,
16
+ TransportError,
17
+ ValidationError,
18
+ )
19
+ from .models import TaskStatus
20
+ from .openapi import OpenAPISchema, Operation, SchemaError, diff_schemas
21
+ from .streaming import ServerSentEvent
22
+
23
+ __version__ = version("p0aiapi")
24
+ __all__ = [
25
+ "APIError",
26
+ "AsyncClient",
27
+ "AuthenticationError",
28
+ "Client",
29
+ "P0Error",
30
+ "P0ResponseError",
31
+ "NotFoundError",
32
+ "OpenAPISchema",
33
+ "Operation",
34
+ "RateLimitError",
35
+ "SchemaError",
36
+ "ServerError",
37
+ "ServerSentEvent",
38
+ "TaskFailedError",
39
+ "TaskStatus",
40
+ "TaskTimeoutError",
41
+ "TransportError",
42
+ "ValidationError",
43
+ "__version__",
44
+ "diff_schemas",
45
+ "load_catalog",
46
+ ]
p0aiapi/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
p0aiapi/audio.py ADDED
@@ -0,0 +1,117 @@
1
+ """Conveniences for the backend's public audio and TTS v2 APIs.
2
+
3
+ Generation returns the initial task record. List and upload responses remain in
4
+ the server's original shape, including fields introduced by later API versions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import mimetypes
10
+ from os import PathLike
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from .resources import _AsyncClient, _Client, _provided
15
+
16
+
17
+ def _content_type(source: Path) -> str:
18
+ if source.suffix.lower() == ".wav":
19
+ return "audio/wav"
20
+ return mimetypes.guess_type(source.name)[0] or "application/octet-stream"
21
+
22
+
23
+ class Audio:
24
+ """TTS v1 voice management and text-to-speech task submission."""
25
+
26
+ def __init__(self, client: _Client) -> None:
27
+ self._client = client
28
+
29
+ def list_voices(self) -> Any:
30
+ """Return the available voice IDs in the backend's response shape."""
31
+ return self._client.request("GET", "/api/public/generate/audio/list")
32
+
33
+ def generate(self, text: str, **fields: Any) -> Any:
34
+ """Submit audio generation; fields may include spk_id and task_id.
35
+
36
+ Optional values are omitted when None, so the backend controls defaults.
37
+ The SDK sends text unchanged; any further processing belongs to the API.
38
+ """
39
+ return self._client.request(
40
+ "POST", "/api/public/generate/audio", data=_provided({"text": text, **fields})
41
+ )
42
+
43
+ def upload_voice(self, spk_id: str, prompt_text: str, path: str | PathLike[str]) -> Any:
44
+ """Upload a local voice sample with its transcript.
45
+
46
+ The backend currently requires a WAV sample of 5–30 seconds. The local
47
+ file is closed whether the upload succeeds or raises an exception.
48
+ """
49
+ source = Path(path)
50
+ with source.open("rb") as stream:
51
+ return self._client.request(
52
+ "POST",
53
+ "/api/public/generate/audio/upload",
54
+ data={"spk_id": spk_id, "prompt_text": prompt_text},
55
+ files={"prompt_audio": (source.name, stream, _content_type(source))},
56
+ )
57
+
58
+
59
+ class AsyncAudio:
60
+ """Asynchronous counterpart of :class:`Audio`."""
61
+
62
+ def __init__(self, client: _AsyncClient) -> None:
63
+ self._client = client
64
+
65
+ async def list_voices(self) -> Any:
66
+ return await self._client.request("GET", "/api/public/generate/audio/list")
67
+
68
+ async def generate(self, text: str, **fields: Any) -> Any:
69
+ return await self._client.request(
70
+ "POST", "/api/public/generate/audio", data=_provided({"text": text, **fields})
71
+ )
72
+
73
+ async def upload_voice(self, spk_id: str, prompt_text: str, path: str | PathLike[str]) -> Any:
74
+ """Upload a WAV sample and transcript; close the file after completion."""
75
+ source = Path(path)
76
+ with source.open("rb") as stream:
77
+ return await self._client.request(
78
+ "POST",
79
+ "/api/public/generate/audio/upload",
80
+ data={"spk_id": spk_id, "prompt_text": prompt_text},
81
+ files={"prompt_audio": (source.name, stream, _content_type(source))},
82
+ )
83
+
84
+
85
+ class TTS:
86
+ """TTS v2 speaker management and text-to-speech task submission."""
87
+
88
+ def __init__(self, client: _Client) -> None:
89
+ self._client = client
90
+
91
+ def list_speakers(self) -> Any:
92
+ return self._client.request("GET", "/api/public/generate/ttsv2/speakers")
93
+
94
+ def generate(self, text: str, spk_id: str, **fields: Any) -> Any:
95
+ """Submit TTS v2; optional fields include use_emo, emo_type, and task_id."""
96
+ return self._client.request(
97
+ "POST",
98
+ "/api/public/generate/ttsv2",
99
+ data=_provided({"text": text, "spk_id": spk_id, **fields}),
100
+ )
101
+
102
+
103
+ class AsyncTTS:
104
+ """Asynchronous counterpart of :class:`TTS`."""
105
+
106
+ def __init__(self, client: _AsyncClient) -> None:
107
+ self._client = client
108
+
109
+ async def list_speakers(self) -> Any:
110
+ return await self._client.request("GET", "/api/public/generate/ttsv2/speakers")
111
+
112
+ async def generate(self, text: str, spk_id: str, **fields: Any) -> Any:
113
+ return await self._client.request(
114
+ "POST",
115
+ "/api/public/generate/ttsv2",
116
+ data=_provided({"text": text, "spk_id": spk_id, **fields}),
117
+ )
p0aiapi/catalog.py ADDED
@@ -0,0 +1,21 @@
1
+ """Load the packaged, reviewed catalogue without a network request.
2
+
3
+ The catalogue covers a supported subset of YApi project 8 matched to registered
4
+ backend routes. It is neither the full backend API nor a live deployment schema.
5
+ Use an explicitly fetched deployment schema when current server metadata is
6
+ needed. Catalogue IDs such as ``yapi_2527`` remain independent of FastAPI's
7
+ generated operation IDs.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from importlib.resources import files
14
+
15
+ from .openapi import OpenAPISchema
16
+
17
+
18
+ def load_catalog() -> OpenAPISchema:
19
+ """Return a fresh offline schema containing the 27 reviewed YApi operations."""
20
+ resource = files("p0aiapi").joinpath("schemas").joinpath("yapi-8.openapi.json")
21
+ return OpenAPISchema(json.loads(resource.read_text(encoding="utf-8")))
p0aiapi/cli.py ADDED
@@ -0,0 +1,78 @@
1
+ """Download and inspect deployment-specific OpenAPI snapshots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+ from .catalog import load_catalog
13
+ from .client import DEFAULT_BASE_URL, Client
14
+ from .exceptions import P0Error
15
+ from .openapi import OpenAPISchema, diff_schemas
16
+
17
+
18
+ def _write_snapshot(path: Path, document: dict) -> None:
19
+ path.parent.mkdir(parents=True, exist_ok=True)
20
+ filename = None
21
+ try:
22
+ with tempfile.NamedTemporaryFile(
23
+ mode="w", encoding="utf-8", dir=path.parent, delete=False
24
+ ) as file:
25
+ filename = file.name
26
+ json.dump(document, file, ensure_ascii=False, indent=2)
27
+ file.write("\n")
28
+ os.replace(filename, path)
29
+ finally:
30
+ if filename is not None and os.path.exists(filename):
31
+ os.unlink(filename)
32
+
33
+
34
+ def main(argv: list[str] | None = None) -> int:
35
+ parser = argparse.ArgumentParser(prog="p0aiapi", description="P0 OpenAPI snapshot tools")
36
+ sub = parser.add_subparsers(dest="command", required=True)
37
+ fetch = sub.add_parser("fetch-schema", help="Download and validate /openapi.json")
38
+ fetch.add_argument(
39
+ "--base-url", help=f"Service root; defaults to P0_API_BASE_URL or {DEFAULT_BASE_URL}"
40
+ )
41
+ fetch.add_argument("--path", default="/openapi.json", help="Schema path inside this deployment")
42
+ fetch.add_argument("--output", required=True, type=Path)
43
+ listing = sub.add_parser("operations", help="List operation IDs, HTTP methods and paths")
44
+ listing.add_argument("schema", type=Path, nargs="?", help="Defaults to the bundled catalog")
45
+ export = sub.add_parser("export-catalog", help="Save the bundled, verified YApi subset")
46
+ export.add_argument("--output", required=True, type=Path)
47
+ diff = sub.add_parser("diff", help="Compare two schema snapshots (not a compatibility proof)")
48
+ diff.add_argument("old", type=Path)
49
+ diff.add_argument("new", type=Path)
50
+ diff.add_argument("--fail-on-change", action="store_true", help="Exit 2 on any difference")
51
+ args = parser.parse_args(argv)
52
+ try:
53
+ if args.command == "fetch-schema":
54
+ with Client(args.base_url) as client:
55
+ schema = client.refresh_openapi(args.path)
56
+ _write_snapshot(args.output, schema.document)
57
+ print(f"Saved {len(schema.operations)} operations to {args.output}")
58
+ elif args.command == "operations":
59
+ schema = OpenAPISchema.load(args.schema) if args.schema else load_catalog()
60
+ for operation in schema.operations.values():
61
+ print(f"{operation.id}\t{operation.method}\t{operation.path}")
62
+ elif args.command == "export-catalog":
63
+ schema = load_catalog()
64
+ _write_snapshot(args.output, schema.document)
65
+ print(f"Saved {len(schema.operations)} bundled operations to {args.output}")
66
+ else:
67
+ result = diff_schemas(OpenAPISchema.load(args.old), OpenAPISchema.load(args.new))
68
+ print(json.dumps(result, ensure_ascii=False, indent=2))
69
+ if args.fail_on_change and any(result.values()):
70
+ return 2
71
+ except (P0Error, ValueError, OSError) as exc:
72
+ print(f"Error: {exc}", file=sys.stderr)
73
+ return 1
74
+ return 0
75
+
76
+
77
+ if __name__ == "__main__":
78
+ raise SystemExit(main())