xtream-api-client 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.
@@ -0,0 +1,38 @@
1
+ """Xtream API Client - a small library for Xtream-style IPTV API endpoints.
2
+
3
+ This package provides a Python interface for interacting with servers that
4
+ implement the Xtream Codes-style API:
5
+
6
+ * configuring and normalizing a server base URL
7
+ * authenticating with a username/password pair supplied by the caller
8
+ * retrieving account information
9
+ * retrieving live, VOD, and series categories
10
+ * retrieving basic stream lists
11
+ * exporting results as JSON
12
+ """
13
+
14
+ from .client import XtreamClient
15
+ from .exceptions import (
16
+ XtreamAPIError,
17
+ XtreamAuthenticationError,
18
+ XtreamConnectionError,
19
+ XtreamHTTPError,
20
+ XtreamTimeoutError,
21
+ XtreamURLValidationError,
22
+ )
23
+ from .models import AccountInfo, Category, Channel
24
+
25
+ __all__ = [
26
+ "XtreamClient",
27
+ "XtreamAPIError",
28
+ "XtreamAuthenticationError",
29
+ "XtreamConnectionError",
30
+ "XtreamHTTPError",
31
+ "XtreamTimeoutError",
32
+ "XtreamURLValidationError",
33
+ "AccountInfo",
34
+ "Category",
35
+ "Channel",
36
+ ]
37
+
38
+ __version__ = "0.1.0"
@@ -0,0 +1,249 @@
1
+ """HTTP client for Xtream-style API endpoints.
2
+
3
+ The client uses only the Python standard library (:mod:`urllib.request`)
4
+ so the package has no runtime dependencies. All responses are parsed into
5
+ Python dictionaries or lists; helper methods convert the most common
6
+ responses into the dataclasses from :mod:`xtream_api_client.models`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import time
13
+ import urllib.error
14
+ import urllib.parse
15
+ import urllib.request
16
+ from typing import Any, Dict, Iterable, List, Optional, Union
17
+
18
+ from .exceptions import (
19
+ XtreamAPIError,
20
+ XtreamAuthenticationError,
21
+ XtreamConnectionError,
22
+ XtreamHTTPError,
23
+ XtreamTimeoutError,
24
+ XtreamURLValidationError,
25
+ )
26
+ from .models import AccountInfo, Category, Channel
27
+
28
+ __all__ = ["XtreamClient", "normalize_server_url"]
29
+
30
+ # Actions supported by the player_api endpoint of Xtream-style servers.
31
+ _DEFAULT_ACTION_KEY = "action"
32
+
33
+ _USER_AGENT = "xtream-api-client/0.1.0"
34
+
35
+
36
+ def normalize_server_url(server_url: str) -> str:
37
+ """Validate and normalize a server base URL.
38
+
39
+ The function accepts URLs with or without a scheme, strips trailing
40
+ slashes and whitespace, and requires a hostname.
41
+
42
+ Args:
43
+ server_url: The base URL of an Xtream-compatible server, e.g.
44
+ ``"http://example.com:8080"`` or ``"example.com:8080"``.
45
+
46
+ Returns:
47
+ The normalized URL without a trailing slash. If no scheme was
48
+ given, ``http://`` is assumed.
49
+
50
+ Raises:
51
+ XtreamURLValidationError: If the URL is empty, has no hostname,
52
+ or uses a scheme other than ``http``/``https``.
53
+ """
54
+ if not server_url or not server_url.strip():
55
+ raise XtreamURLValidationError("Server URL must not be empty.")
56
+
57
+ url = server_url.strip()
58
+ if "://" not in url:
59
+ url = "http://" + url
60
+
61
+ parsed = urllib.parse.urlparse(url)
62
+ if parsed.scheme not in ("http", "https"):
63
+ raise XtreamURLValidationError(
64
+ f"Unsupported URL scheme {parsed.scheme!r}; expected http or https."
65
+ )
66
+ if not parsed.netloc:
67
+ raise XtreamURLValidationError(f"URL {server_url!r} has no hostname.")
68
+
69
+ return url.rstrip("/")
70
+
71
+
72
+ class XtreamClient:
73
+ """A small client for Xtream-style IPTV API endpoints.
74
+
75
+ Example:
76
+ >>> client = XtreamClient("http://example.com:8080", "user", "pass")
77
+ >>> info = client.get_account_info() # doctest: +SKIP
78
+
79
+ Args:
80
+ server_url: Base URL of the server (with or without scheme).
81
+ username: Username supplied by the user or their provider.
82
+ password: Password supplied by the user or their provider.
83
+ timeout: Request timeout in seconds (default 10.0).
84
+ verify_input: When True (default), URLs are normalized up front.
85
+ """
86
+
87
+ def __init__(
88
+ self,
89
+ server_url: str,
90
+ username: str,
91
+ password: str,
92
+ timeout: float = 10.0,
93
+ verify_input: bool = True,
94
+ ) -> None:
95
+ if verify_input:
96
+ self.server_url = normalize_server_url(server_url)
97
+ else:
98
+ self.server_url = (server_url or "").rstrip("/")
99
+ if not username or not password:
100
+ raise XtreamAuthenticationError("Username and password are required.")
101
+ self.username = username
102
+ self.password = password
103
+ self.timeout = float(timeout)
104
+
105
+ # ------------------------------------------------------------------
106
+ # Low-level request machinery
107
+ # ------------------------------------------------------------------
108
+
109
+ def _build_url(self, action: Optional[str] = None, extra_params: Optional[Dict[str, Any]] = None) -> str:
110
+ """Construct a player_api URL for the given action."""
111
+ params: Dict[str, Any] = {
112
+ "username": self.username,
113
+ "password": self.password,
114
+ }
115
+ if action is not None:
116
+ params[_DEFAULT_ACTION_KEY] = action
117
+ if extra_params:
118
+ params.update(extra_params)
119
+ query = urllib.parse.urlencode(params)
120
+ return f"{self.server_url}/player_api.php?{query}"
121
+
122
+ def _request_json(self, url: str) -> Union[Dict[str, Any], List[Any]]:
123
+ """Perform a GET request and parse the JSON body.
124
+
125
+ Raises:
126
+ XtreamTimeoutError: On timeout.
127
+ XtreamConnectionError: When the server cannot be reached.
128
+ XtreamHTTPError: On non-200 HTTP responses.
129
+ XtreamAPIError: When the body is not valid JSON.
130
+ """
131
+ req = urllib.request.Request(url, headers={"User-Agent": _USER_AGENT})
132
+ try:
133
+ with urllib.request.urlopen(req, timeout=self.timeout) as response:
134
+ body = response.read()
135
+ except urllib.error.HTTPError as exc:
136
+ raise XtreamHTTPError(exc.code, url) from exc
137
+ except urllib.error.URLError as exc:
138
+ reason = getattr(exc, "reason", exc)
139
+ if isinstance(reason, TimeoutError) or "timed out" in str(reason).lower():
140
+ raise XtreamTimeoutError(f"Request to {url} timed out.") from exc
141
+ raise XtreamConnectionError(f"Cannot reach {url}: {reason}") from exc
142
+ except TimeoutError as exc:
143
+ raise XtreamTimeoutError(f"Request to {url} timed out.") from exc
144
+
145
+ try:
146
+ data = json.loads(body.decode("utf-8", errors="replace"))
147
+ except (json.JSONDecodeError, UnicodeDecodeError) as exc:
148
+ raise XtreamAPIError(f"Server returned non-JSON content from {url}.") from exc
149
+ if not isinstance(data, (dict, list)):
150
+ raise XtreamAPIError(f"Unexpected JSON shape from {url}.")
151
+ return data
152
+
153
+ def _call(self, action: Optional[str] = None, extra_params: Optional[Dict[str, Any]] = None) -> Any:
154
+ """Call the player_api endpoint and apply basic auth checks."""
155
+ url = self._build_url(action=action, extra_params=extra_params)
156
+ data = self._request_json(url)
157
+ if isinstance(data, dict):
158
+ auth = data.get("user_info", {})
159
+ if isinstance(auth, dict) and auth.get("auth") in (0, False, "0"):
160
+ raise XtreamAuthenticationError(
161
+ "Server rejected the supplied credentials."
162
+ )
163
+ return data
164
+
165
+ # ------------------------------------------------------------------
166
+ # Public API
167
+ # ------------------------------------------------------------------
168
+
169
+ def get_account_info(self) -> Dict[str, Any]:
170
+ """Return the raw account/user information dictionary.
171
+
172
+ Makes an unauthenticated-action call to ``player_api.php`` and
173
+ returns the parsed JSON, which typically contains ``user_info``
174
+ and ``server_info`` keys.
175
+ """
176
+ return self._call()
177
+
178
+ def get_account_info_parsed(self) -> AccountInfo:
179
+ """Return parsed :class:`~xtream_api_client.models.AccountInfo`."""
180
+ return AccountInfo.from_dict(self._call())
181
+
182
+ def get_live_categories(self) -> List[Dict[str, Any]]:
183
+ """Return live TV categories as a list of dictionaries."""
184
+ return self._call("get_live_categories") # type: ignore[return-value]
185
+
186
+ def get_vod_categories(self) -> List[Dict[str, Any]]:
187
+ """Return VOD (movie) categories as a list of dictionaries."""
188
+ return self._call("get_vod_categories") # type: ignore[return-value]
189
+
190
+ def get_series_categories(self) -> List[Dict[str, Any]]:
191
+ """Return series categories as a list of dictionaries."""
192
+ return self._call("get_series_categories") # type: ignore[return-value]
193
+
194
+ def get_live_streams(self, category_id: Optional[str] = None) -> List[Dict[str, Any]]:
195
+ """Return the live channel list, optionally filtered by category."""
196
+ params = {"category_id": category_id} if category_id else None
197
+ return self._call("get_live_streams", extra_params=params) # type: ignore[return-value]
198
+
199
+ def get_vod_streams(self, category_id: Optional[str] = None) -> List[Dict[str, Any]]:
200
+ """Return the VOD stream list, optionally filtered by category."""
201
+ params = {"category_id": category_id} if category_id else None
202
+ return self._call("get_vod_streams", extra_params=params) # type: ignore[return-value]
203
+
204
+ def get_series(self, category_id: Optional[str] = None) -> List[Dict[str, Any]]:
205
+ """Return the series list, optionally filtered by category."""
206
+ params = {"category_id": category_id} if category_id else None
207
+ return self._call("get_series", extra_params=params) # type: ignore[return-value]
208
+
209
+ # ------------------------------------------------------------------
210
+ # Model-based helpers
211
+ # ------------------------------------------------------------------
212
+
213
+ def list_live_categories(self) -> List[Category]:
214
+ """Return live categories as :class:`Category` objects."""
215
+ return [Category.from_dict(c, "live") for c in self.get_live_categories()]
216
+
217
+ def list_vod_categories(self) -> List[Category]:
218
+ """Return VOD categories as :class:`Category` objects."""
219
+ return [Category.from_dict(c, "vod") for c in self.get_vod_categories()]
220
+
221
+ def list_series_categories(self) -> List[Category]:
222
+ """Return series categories as :class:`Category` objects."""
223
+ return [Category.from_dict(c, "series") for c in self.get_series_categories()]
224
+
225
+ def list_live_streams(self, category_id: Optional[str] = None) -> List[Channel]:
226
+ """Return live streams as :class:`Channel` objects."""
227
+ return [Channel.from_dict(s) for s in self.get_live_streams(category_id)]
228
+
229
+ # ------------------------------------------------------------------
230
+ # Export helpers
231
+ # ------------------------------------------------------------------
232
+
233
+ def export_json(self, data: Any, path: Optional[str] = None, indent: int = 2) -> Union[str, None]:
234
+ """Serialize results to a JSON string and optionally write to a file.
235
+
236
+ Args:
237
+ data: Any JSON-serializable result from the client methods.
238
+ path: When given, the JSON is also written to this file path.
239
+ indent: Indentation level for the output (default 2).
240
+
241
+ Returns:
242
+ The JSON string, or ``None`` when ``path`` is given.
243
+ """
244
+ text = json.dumps(data, indent=indent, ensure_ascii=False, default=str)
245
+ if path:
246
+ with open(path, "w", encoding="utf-8") as fh:
247
+ fh.write(text)
248
+ return None
249
+ return text
@@ -0,0 +1,40 @@
1
+ """Exception classes for the xtream_api_client package."""
2
+
3
+
4
+ class XtreamAPIError(Exception):
5
+ """Base exception for all errors raised by this library.
6
+
7
+ Callers can catch this single type to handle any failure produced
8
+ by :class:`~xtream_api_client.XtreamClient`.
9
+ """
10
+
11
+
12
+ class XtreamURLValidationError(XtreamAPIError):
13
+ """Raised when the server URL is missing, malformed, or unsupported."""
14
+
15
+
16
+ class XtreamAuthenticationError(XtreamAPIError):
17
+ """Raised when the server rejects the supplied username/password."""
18
+
19
+
20
+ class XtreamTimeoutError(XtreamAPIError):
21
+ """Raised when a request exceeds the configured timeout."""
22
+
23
+
24
+ class XtreamConnectionError(XtreamAPIError):
25
+ """Raised when the server cannot be reached (DNS, refused, etc.)."""
26
+
27
+
28
+ class XtreamHTTPError(XtreamAPIError):
29
+ """Raised when the server returns an unexpected HTTP status code.
30
+
31
+ Attributes:
32
+ status_code: The HTTP status code returned by the server.
33
+ url: The request URL that produced the error.
34
+ """
35
+
36
+ def __init__(self, status_code: int, url: str, message: str = "") -> None:
37
+ self.status_code = status_code
38
+ self.url = url
39
+ detail = message or f"HTTP {status_code} from {url}"
40
+ super().__init__(detail)
@@ -0,0 +1,123 @@
1
+ """Lightweight data models returned by the client.
2
+
3
+ The raw API responses are plain dictionaries, and :meth:`dict` access is
4
+ always available. These models add attribute-style access and light
5
+ normalization for the most commonly used fields.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Dict, Optional
12
+
13
+
14
+ @dataclass
15
+ class AccountInfo:
16
+ """Parsed account information returned by the ``player_api`` login check.
17
+
18
+ Attributes:
19
+ username: The account username.
20
+ status: Server-reported account status (for example ``"Active"``).
21
+ exp_date: Optional expiration date as a Unix timestamp string.
22
+ is_trial: Whether the server reports this as a trial account.
23
+ active_connections: Currently active connections, when reported.
24
+ max_connections: Maximum allowed connections, when reported.
25
+ raw: The complete, unparsed response dictionary.
26
+ """
27
+
28
+ username: str
29
+ status: Optional[str] = None
30
+ exp_date: Optional[str] = None
31
+ is_trial: Optional[bool] = None
32
+ active_connections: Optional[int] = None
33
+ max_connections: Optional[int] = None
34
+ raw: Dict[str, Any] = field(default_factory=dict)
35
+
36
+ @classmethod
37
+ def from_dict(cls, data: Dict[str, Any]) -> "AccountInfo":
38
+ """Build an :class:`AccountInfo` from a raw ``user_info`` dictionary."""
39
+ user = data.get("user_info", data) if isinstance(data, dict) else {}
40
+ return cls(
41
+ username=str(user.get("username", "")),
42
+ status=user.get("status"),
43
+ exp_date=user.get("exp_date"),
44
+ is_trial=_to_bool(user.get("is_trial")),
45
+ active_connections=_to_int(user.get("active_cons")),
46
+ max_connections=_to_int(user.get("max_connections")),
47
+ raw=dict(data),
48
+ )
49
+
50
+
51
+ @dataclass
52
+ class Category:
53
+ """A single category entry (live, VOD, or series).
54
+
55
+ Attributes:
56
+ category_id: The server-side category identifier.
57
+ name: Human-readable category name.
58
+ category_type: One of ``"live"``, ``"vod"``, or ``"series"``.
59
+ """
60
+
61
+ category_id: str
62
+ name: str
63
+ category_type: str = "live"
64
+
65
+ @classmethod
66
+ def from_dict(cls, data: Dict[str, Any], category_type: str = "live") -> "Category":
67
+ """Build a :class:`Category` from a raw category dictionary."""
68
+ return cls(
69
+ category_id=str(data.get("category_id", "")),
70
+ name=str(data.get("category_name", "")),
71
+ category_type=category_type,
72
+ )
73
+
74
+
75
+ @dataclass
76
+ class Channel:
77
+ """A single live channel / VOD item from a stream list.
78
+
79
+ Attributes:
80
+ stream_id: The server-side stream identifier.
81
+ name: Human-readable stream name.
82
+ category_id: Category this stream belongs to.
83
+ stream_type: Stream type reported by the server, when present.
84
+ raw: The complete, unparsed entry dictionary.
85
+ """
86
+
87
+ stream_id: str
88
+ name: str
89
+ category_id: Optional[str] = None
90
+ stream_type: Optional[str] = None
91
+ raw: Dict[str, Any] = field(default_factory=dict)
92
+
93
+ @classmethod
94
+ def from_dict(cls, data: Dict[str, Any]) -> "Channel":
95
+ """Build a :class:`Channel` from a raw stream-list entry."""
96
+ return cls(
97
+ stream_id=str(data.get("stream_id", data.get("series_id", ""))),
98
+ name=str(data.get("name", "")),
99
+ category_id=data.get("category_id"),
100
+ stream_type=data.get("stream_type"),
101
+ raw=dict(data),
102
+ )
103
+
104
+
105
+ def _to_bool(value: Any) -> Optional[bool]:
106
+ """Coerce common server representations of booleans."""
107
+ if value is None:
108
+ return None
109
+ if isinstance(value, bool):
110
+ return value
111
+ if isinstance(value, str):
112
+ return value.strip().lower() in ("true", "1", "yes")
113
+ return bool(value)
114
+
115
+
116
+ def _to_int(value: Any) -> Optional[int]:
117
+ """Coerce common server representations of integers, returning None on failure."""
118
+ if value is None or value == "":
119
+ return None
120
+ try:
121
+ return int(value)
122
+ except (TypeError, ValueError):
123
+ return None
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.4
2
+ Name: xtream-api-client
3
+ Version: 0.1.0
4
+ Summary: A small Python client for Xtream-style IPTV API endpoints using only the standard library.
5
+ Author: XtreamTech
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://xtreamtech.net
8
+ Project-URL: Documentation, https://xtreamtech.net
9
+ Keywords: xtream,iptv,api,client,player_api
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Topic :: Internet
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: test
25
+ Requires-Dist: pytest>=7.0; extra == "test"
26
+ Dynamic: license-file
27
+
28
+ # xtream-api-client
29
+
30
+ A small Python client for working with Xtream-style IPTV API endpoints (`player_api.php`). It is built entirely on the Python standard library — no third-party runtime dependencies — and returns plain dictionaries, lists, and optional lightweight dataclass models.
31
+
32
+ The library handles URL validation/normalization, query construction, JSON parsing, and error mapping so you can work with an Xtream-compatible server from a few lines of Python.
33
+
34
+ ## What the package does
35
+
36
+ - Configures and validates an Xtream-compatible server base URL
37
+ - Authenticates with a username/password pair that you supply
38
+ - Retrieves account information
39
+ - Retrieves live, VOD, and series categories
40
+ - Retrieves basic live, VOD, and series stream lists
41
+ - Maps HTTP, connection, timeout, and authentication failures to dedicated exception classes
42
+ - Exports any result as JSON (to a string or a file)
43
+
44
+ This client only calls documented read-style endpoints (`player_api.php` actions). It does not implement stream playback, does not bypass authentication, and does not attempt to circumvent any provider restriction.
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ pip install xtream-api-client
50
+ ```
51
+
52
+ Requires Python 3.9 or newer.
53
+
54
+ Or from source:
55
+
56
+ ```bash
57
+ git clone <your-repo-url>
58
+ cd xtreamtech
59
+ py -m pip install -e .
60
+ ```
61
+
62
+ ## Quick-start example
63
+
64
+ ```python
65
+ from xtream_api_client import XtreamClient
66
+
67
+ client = XtreamClient(
68
+ server_url="http://example.com:8080",
69
+ username="your-username",
70
+ password="your-password",
71
+ )
72
+
73
+ account = client.get_account_info()
74
+ print(account["user_info"]["status"])
75
+
76
+ for category in client.get_live_categories():
77
+ print(category["category_id"], category["category_name"])
78
+ ```
79
+
80
+ ## Configuration
81
+
82
+ `XtreamClient` accepts:
83
+
84
+ | Parameter | Type | Default | Description |
85
+ |---|---|---|---|
86
+ | `server_url` | `str` | required | Base URL, with or without a scheme (`http://` is assumed). Trailing slashes and whitespace are stripped. |
87
+ | `username` | `str` | required | Your account username. |
88
+ | `password` | `str` | required | Your account password. |
89
+ | `timeout` | `float` | `10.0` | Per-request timeout in seconds. |
90
+ | `verify_input` | `bool` | `True` | Validate/normalize the URL on construction. |
91
+
92
+ ## Authentication
93
+
94
+ Credentials are sent as query parameters on every `player_api.php` request, matching the Xtream API convention. If the server reports `user_info.auth` as falsy, an `XtreamAuthenticationError` is raised.
95
+
96
+ There are no hard-coded credentials or servers — supply your own.
97
+
98
+ ## Retrieving account information
99
+
100
+ ```python
101
+ info = client.get_account_info() # raw dict with user_info / server_info
102
+ parsed = client.get_account_info_parsed() # AccountInfo dataclass
103
+ print(parsed.username, parsed.status, parsed.max_connections)
104
+ ```
105
+
106
+ ## Retrieving categories and streams
107
+
108
+ ```python
109
+ client.get_live_categories() # list[dict]
110
+ client.get_vod_categories() # list[dict]
111
+ client.get_series_categories() # list[dict]
112
+
113
+ client.get_live_streams(category_id="1") # optional category filter
114
+ client.get_vod_streams(category_id="2")
115
+ client.get_series(category_id="3")
116
+ ```
117
+
118
+ Model-based equivalents:
119
+
120
+ ```python
121
+ cats = client.list_live_categories() # list[Category]
122
+ streams = client.list_live_streams() # list[Channel]
123
+ ```
124
+
125
+ ## Error handling
126
+
127
+ All exceptions derive from `XtreamAPIError`:
128
+
129
+ | Exception | Raised when |
130
+ |---|---|
131
+ | `XtreamURLValidationError` | The server URL is empty, malformed, or uses an unsupported scheme. |
132
+ | `XtreamAuthenticationError` | Credentials are missing or the server rejects them. |
133
+ | `XtreamTimeoutError` | A request exceeds the configured timeout. |
134
+ | `XtreamConnectionError` | The server cannot be reached (DNS failure, connection refused, etc.). |
135
+ | `XtreamHTTPError` | The server returns a non-200 status (`status_code` and `url` attributes). |
136
+ | `XtreamAPIError` | Base class; also raised for non-JSON or unexpected responses. |
137
+
138
+ ```python
139
+ from xtream_api_client.exceptions import XtreamAPIError, XtreamTimeoutError
140
+
141
+ try:
142
+ categories = client.get_live_categories()
143
+ except XtreamTimeoutError:
144
+ ...
145
+ except XtreamAPIError as exc:
146
+ ...
147
+ ```
148
+
149
+ ## Timeout configuration
150
+
151
+ ```python
152
+ client = XtreamClient(url, user, password, timeout=5.0) # seconds
153
+ ```
154
+
155
+ The timeout applies per request to both connection establishment and reading.
156
+
157
+ ## Exporting results as JSON
158
+
159
+ ```python
160
+ text = client.export_json(client.get_live_categories()) # returns str
161
+ client.export_json(data, path="categories.json") # writes a file
162
+ ```
163
+
164
+ ## Testing
165
+
166
+ Tests use mocked HTTP responses only — no real servers are contacted.
167
+
168
+ ```bash
169
+ py -m pip install -e ".[test]"
170
+ py -m pytest
171
+ ```
172
+
173
+ ## Limitations
174
+
175
+ - Read-style `player_api.php` actions only; no episode/stream URL building or playback.
176
+ - Error detection relies on the server returning well-formed JSON; behavior varies between server implementations and versions.
177
+ - Credentials appear in query strings, matching the API convention; avoid logging full request URLs.
178
+ - Synchronous `urllib` based I/O only; no async support or connection pooling.
179
+
180
+ ## License
181
+
182
+ MIT — see [LICENSE](LICENSE).
183
+
184
+ ## Additional Xtream/IPTV technical resources
185
+
186
+ - Project documentation and resources: [https://xtreamtech.net](https://xtreamtech.net)
187
+ - Python `urllib.request` documentation: https://docs.python.org/3/library/urllib.request.html
@@ -0,0 +1,9 @@
1
+ xtream_api_client/__init__.py,sha256=3heqP4VP9kaUH4GSxN75diCn6fprcZUaB60ZWIbvBPo,991
2
+ xtream_api_client/client.py,sha256=oH4CBm74YXX3qdZpNeH4pztUbokuTJGoy0x0ixUyaGE,10419
3
+ xtream_api_client/exceptions.py,sha256=uvc4mTwFit1mYa5eO3veJFA4kD0A9sjHUeOy86eUqWo,1253
4
+ xtream_api_client/models.py,sha256=SmrUgj9ZRhqHdqc3l9qzeG9q2KCyJavL5nkNXCjZ3Es,4112
5
+ xtream_api_client-0.1.0.dist-info/licenses/LICENSE,sha256=0FFfT2Q1WBS3zQTM74B2X8ns2mYEBe1NnqD_-I-XZOo,1067
6
+ xtream_api_client-0.1.0.dist-info/METADATA,sha256=ClmGQcJPRfCsSUphTAFNNhNaRL521-5F5AiIKpOw23s,6565
7
+ xtream_api_client-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
8
+ xtream_api_client-0.1.0.dist-info/top_level.txt,sha256=XXZLto4CLmQ9ZmBTo94u1jyIyE0GSC7FPtAhX4-CUSY,18
9
+ xtream_api_client-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 XtreamTech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ xtream_api_client