blueticks 1.0.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.
blueticks/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ from blueticks._client import Blueticks
2
+ from blueticks._errors import (
3
+ APIConnectionError,
4
+ APIError,
5
+ AuthenticationError,
6
+ BadRequestError,
7
+ BluetickError,
8
+ NotFoundError,
9
+ PermissionDeniedError,
10
+ RateLimitError,
11
+ )
12
+ from blueticks._version import __version__
13
+
14
+ __all__ = [
15
+ "APIConnectionError",
16
+ "APIError",
17
+ "AuthenticationError",
18
+ "BadRequestError",
19
+ "Blueticks",
20
+ "BluetickError",
21
+ "NotFoundError",
22
+ "PermissionDeniedError",
23
+ "RateLimitError",
24
+ "__version__",
25
+ ]
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from blueticks._client import Blueticks
7
+
8
+
9
+ class BaseResource:
10
+ """Shared plumbing for all resource classes. Holds a reference to the parent client."""
11
+
12
+ def __init__(self, client: Blueticks) -> None:
13
+ self._client = client
blueticks/_client.py ADDED
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ import httpx
7
+
8
+ if TYPE_CHECKING:
9
+ from blueticks.types.ping import Ping
10
+
11
+ from blueticks._errors import BluetickError
12
+ from blueticks._transport import Transport
13
+ from blueticks._version import __version__
14
+
15
+ _DEFAULT_BASE_URL = "https://api.blueticks.co"
16
+ _DEFAULT_TIMEOUT = 30.0
17
+ _DEFAULT_MAX_RETRIES = 2
18
+
19
+
20
+ class Blueticks:
21
+ """Synchronous client for the Blueticks API."""
22
+
23
+ def __init__(
24
+ self,
25
+ api_key: str | None = None,
26
+ *,
27
+ base_url: str | None = None,
28
+ timeout: float = _DEFAULT_TIMEOUT,
29
+ max_retries: int = _DEFAULT_MAX_RETRIES,
30
+ user_agent: str | None = None,
31
+ _http_transport: httpx.BaseTransport | None = None,
32
+ ) -> None:
33
+ resolved_key = api_key if api_key is not None else os.environ.get("BLUETICKS_API_KEY")
34
+ if not resolved_key:
35
+ raise BluetickError(
36
+ status_code=None,
37
+ code="authentication_required",
38
+ message=(
39
+ "No api_key provided. Pass Blueticks(api_key=...) "
40
+ "or set BLUETICKS_API_KEY in your environment."
41
+ ),
42
+ request_id=None,
43
+ )
44
+ self._api_key = resolved_key
45
+
46
+ resolved_base = (
47
+ base_url
48
+ if base_url is not None
49
+ else os.environ.get("BLUETICKS_BASE_URL") or _DEFAULT_BASE_URL
50
+ )
51
+ self._base_url = resolved_base
52
+
53
+ self._transport = Transport(
54
+ api_key=self._api_key,
55
+ base_url=self._base_url,
56
+ timeout=timeout,
57
+ max_retries=max_retries,
58
+ user_agent_suffix=user_agent,
59
+ version=__version__,
60
+ http_transport=_http_transport,
61
+ )
62
+
63
+ # Lazy-imported to avoid import cycles and to keep core import cheap.
64
+ from blueticks.resources.account import AccountResource
65
+
66
+ self.account = AccountResource(self)
67
+
68
+ # -- Public API ----------------------------------------------------------
69
+
70
+ def ping(self) -> Ping:
71
+ from blueticks.resources.ping import PingResource
72
+
73
+ return PingResource(self).retrieve()
74
+
75
+ def close(self) -> None:
76
+ self._transport.close()
77
+
78
+ def __enter__(self) -> Blueticks:
79
+ return self
80
+
81
+ def __exit__(self, *_: Any) -> None:
82
+ self.close()
83
+
84
+ # -- Internal ------------------------------------------------------------
85
+
86
+ def _request(
87
+ self,
88
+ method: str,
89
+ path: str,
90
+ *,
91
+ params: dict[str, Any] | None = None,
92
+ body: dict[str, Any] | None = None,
93
+ idempotency_key: str | None = None,
94
+ ) -> Any:
95
+ return self._transport.request(
96
+ method,
97
+ path,
98
+ params=params,
99
+ body=body,
100
+ idempotency_key=idempotency_key,
101
+ )
blueticks/_errors.py ADDED
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+
8
+ class BluetickError(Exception):
9
+ """Base class for all Blueticks client errors."""
10
+
11
+ status_code: int | None
12
+ code: str | None
13
+ message: str
14
+ request_id: str | None
15
+ response: httpx.Response | None
16
+
17
+ def __init__(
18
+ self,
19
+ *,
20
+ status_code: int | None = None,
21
+ code: str | None = None,
22
+ message: str = "",
23
+ request_id: str | None = None,
24
+ response: httpx.Response | None = None,
25
+ ) -> None:
26
+ self.status_code = status_code
27
+ self.code = code
28
+ self.message = message
29
+ self.request_id = request_id
30
+ self.response = response
31
+ super().__init__(str(self))
32
+
33
+ def __str__(self) -> str:
34
+ return f"{self.status_code} {self.code}: {self.message} (request_id={self.request_id})"
35
+
36
+
37
+ class AuthenticationError(BluetickError):
38
+ """401 — missing or invalid API key."""
39
+
40
+
41
+ class PermissionDeniedError(BluetickError):
42
+ """403 — the key lacks the required scope or workspace access."""
43
+
44
+
45
+ class NotFoundError(BluetickError):
46
+ """404 — the resource does not exist."""
47
+
48
+
49
+ class BadRequestError(BluetickError):
50
+ """400 / 422 — the request was malformed or failed validation."""
51
+
52
+
53
+ class RateLimitError(BluetickError):
54
+ """429 — the client is being throttled."""
55
+
56
+ retry_after: float | None
57
+
58
+ def __init__(self, *, retry_after: float | None = None, **kwargs: Any) -> None:
59
+ super().__init__(**kwargs)
60
+ self.retry_after = retry_after
61
+
62
+
63
+ class APIError(BluetickError):
64
+ """5xx or otherwise-unparseable error response from the API."""
65
+
66
+
67
+ class APIConnectionError(BluetickError):
68
+ """Network failure: DNS, TLS, timeout, connection reset."""
69
+
70
+
71
+ _STATUS_TO_CLASS: dict[int, type[BluetickError]] = {
72
+ 400: BadRequestError,
73
+ 401: AuthenticationError,
74
+ 403: PermissionDeniedError,
75
+ 404: NotFoundError,
76
+ 422: BadRequestError,
77
+ 429: RateLimitError,
78
+ }
79
+
80
+
81
+ def _class_for_status(status: int) -> type[BluetickError]:
82
+ return _STATUS_TO_CLASS.get(status, APIError)
83
+
84
+
85
+ def from_envelope(
86
+ *,
87
+ status_code: int,
88
+ body: Any,
89
+ response: httpx.Response | None,
90
+ retry_after: float | None = None,
91
+ ) -> BluetickError:
92
+ """Map a non-2xx response body to a typed Blueticks exception."""
93
+ cls = _class_for_status(status_code)
94
+
95
+ error = body.get("error") if isinstance(body, dict) else None
96
+ if isinstance(error, dict) and isinstance(error.get("code"), str):
97
+ kwargs: dict[str, Any] = {
98
+ "status_code": status_code,
99
+ "code": error.get("code"),
100
+ "message": error.get("message", ""),
101
+ "request_id": error.get("request_id"),
102
+ "response": response,
103
+ }
104
+ if cls is RateLimitError:
105
+ kwargs["retry_after"] = retry_after
106
+ return cls(**kwargs)
107
+
108
+ # Fallback: unknown shape. Render the body as a truncated string.
109
+ raw = body if isinstance(body, str) else repr(body)
110
+ truncated = raw[:200] + ("..." if len(raw) > 200 else "")
111
+ kwargs = {
112
+ "status_code": status_code,
113
+ "code": None,
114
+ "message": truncated,
115
+ "request_id": None,
116
+ "response": response,
117
+ }
118
+ if cls is RateLimitError:
119
+ kwargs["retry_after"] = retry_after
120
+ return cls(**kwargs)
@@ -0,0 +1,141 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import random
5
+ import time
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ from blueticks._errors import (
11
+ APIConnectionError,
12
+ from_envelope,
13
+ )
14
+
15
+ _RETRIABLE_STATUS = {429, 502, 503, 504}
16
+ _IDEMPOTENT_METHODS = {"GET", "HEAD", "OPTIONS", "DELETE", "PATCH", "PUT"}
17
+ # Note: PATCH is not strictly idempotent per RFC 5789 but most REST APIs treat
18
+ # partial updates as idempotent in practice. Matches Stripe/OpenAI convention.
19
+ _BACKOFF_BASE_SECONDS = 0.5
20
+ _BACKOFF_CAP_SECONDS = 8.0
21
+
22
+
23
+ class Transport:
24
+ """HTTP transport for the Blueticks API. Wraps httpx.Client with retries."""
25
+
26
+ def __init__(
27
+ self,
28
+ *,
29
+ api_key: str,
30
+ base_url: str,
31
+ timeout: float,
32
+ max_retries: int,
33
+ user_agent_suffix: str | None,
34
+ version: str,
35
+ http_transport: httpx.BaseTransport | None = None,
36
+ ) -> None:
37
+ self._api_key = api_key
38
+ self._base_url = base_url.rstrip("/")
39
+ self._max_retries = max_retries
40
+ self._version = version
41
+ self._user_agent = f"blueticks-python/{version}"
42
+ if user_agent_suffix:
43
+ self._user_agent = f"{self._user_agent} {user_agent_suffix}"
44
+ self._client = httpx.Client(
45
+ base_url=self._base_url,
46
+ timeout=timeout,
47
+ transport=http_transport,
48
+ )
49
+
50
+ def close(self) -> None:
51
+ self._client.close()
52
+
53
+ def request(
54
+ self,
55
+ method: str,
56
+ path: str,
57
+ *,
58
+ params: dict[str, Any] | None,
59
+ body: dict[str, Any] | None,
60
+ idempotency_key: str | None = None,
61
+ ) -> Any:
62
+ method = method.upper()
63
+ headers: dict[str, str] = {
64
+ "Authorization": f"Bearer {self._api_key}",
65
+ "User-Agent": self._user_agent,
66
+ "Accept": "application/json",
67
+ }
68
+ if body is not None:
69
+ headers["Content-Type"] = "application/json"
70
+ if idempotency_key:
71
+ headers["Idempotency-Key"] = idempotency_key
72
+
73
+ is_idempotent = method in _IDEMPOTENT_METHODS or idempotency_key is not None
74
+
75
+ attempt = 0
76
+ while True:
77
+ try:
78
+ response = self._client.request(
79
+ method,
80
+ path,
81
+ params=params,
82
+ content=json.dumps(body).encode() if body is not None else None,
83
+ headers=headers,
84
+ )
85
+ except httpx.TransportError as exc:
86
+ if attempt < self._max_retries:
87
+ self._sleep_backoff(attempt, retry_after=None)
88
+ attempt += 1
89
+ continue
90
+ raise APIConnectionError(
91
+ status_code=None,
92
+ code=None,
93
+ message=f"connection error: {exc}",
94
+ request_id=None,
95
+ response=None,
96
+ ) from exc
97
+
98
+ if 200 <= response.status_code < 300:
99
+ if not response.content:
100
+ return None
101
+ return response.json()
102
+
103
+ parsed_body: Any
104
+ try:
105
+ parsed_body = response.json()
106
+ except (json.JSONDecodeError, ValueError):
107
+ parsed_body = response.text
108
+
109
+ retry_after = self._parse_retry_after(response) if response.status_code == 429 else None
110
+
111
+ retriable_status = response.status_code in _RETRIABLE_STATUS
112
+ if retriable_status and is_idempotent and attempt < self._max_retries:
113
+ self._sleep_backoff(attempt, retry_after=retry_after)
114
+ attempt += 1
115
+ continue
116
+
117
+ raise from_envelope(
118
+ status_code=response.status_code,
119
+ body=parsed_body,
120
+ response=response,
121
+ retry_after=retry_after,
122
+ )
123
+
124
+ @staticmethod
125
+ def _parse_retry_after(response: httpx.Response) -> float | None:
126
+ raw = response.headers.get("retry-after")
127
+ if raw is None:
128
+ return None
129
+ try:
130
+ return float(raw)
131
+ except ValueError:
132
+ return None
133
+
134
+ @staticmethod
135
+ def _sleep_backoff(attempt: int, *, retry_after: float | None) -> None:
136
+ if retry_after is not None:
137
+ time.sleep(retry_after)
138
+ return
139
+ backoff = min(_BACKOFF_BASE_SECONDS * (2**attempt), _BACKOFF_CAP_SECONDS)
140
+ jitter = random.uniform(0, _BACKOFF_BASE_SECONDS)
141
+ time.sleep(backoff + jitter)
blueticks/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
blueticks/py.typed ADDED
File without changes
@@ -0,0 +1,4 @@
1
+ from blueticks.resources.account import AccountResource
2
+ from blueticks.resources.ping import PingResource
3
+
4
+ __all__ = ["AccountResource", "PingResource"]
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from blueticks._base_resource import BaseResource
4
+ from blueticks.types.account import Account
5
+
6
+
7
+ class AccountResource(BaseResource):
8
+ def retrieve(self) -> Account:
9
+ """Retrieve the authenticated account.
10
+
11
+ Returns the account associated with the API key used for this request.
12
+ """
13
+ data = self._client._request("GET", "/v1/account")
14
+ return Account.model_validate(data)
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from blueticks._base_resource import BaseResource
4
+ from blueticks.types.ping import Ping
5
+
6
+
7
+ class PingResource(BaseResource):
8
+ def retrieve(self) -> Ping:
9
+ """Health check.
10
+
11
+ Returns basic info about the authenticated API key.
12
+ """
13
+ data = self._client._request("GET", "/v1/ping")
14
+ return Ping.model_validate(data)
@@ -0,0 +1,4 @@
1
+ from blueticks.types.account import Account
2
+ from blueticks.types.ping import Ping
3
+
4
+ __all__ = ["Account", "Ping"]
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Optional
5
+
6
+ from pydantic import BaseModel, ConfigDict
7
+
8
+
9
+ class Account(BaseModel):
10
+ """Response from GET /v1/account."""
11
+
12
+ model_config = ConfigDict(extra="ignore")
13
+
14
+ id: str
15
+ name: str
16
+ timezone: Optional[str] = None # noqa: UP045
17
+ created_at: datetime
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, ConfigDict
4
+
5
+
6
+ class Ping(BaseModel):
7
+ """Response from GET /v1/ping."""
8
+
9
+ model_config = ConfigDict(extra="ignore")
10
+
11
+ account_id: str
12
+ key_prefix: str
13
+ scopes: list[str]
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: blueticks
3
+ Version: 1.0.0
4
+ Summary: Official Python client for the Blueticks API.
5
+ Author-email: Blueticks <support@blueticks.co>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 serenix-com
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://blueticks.co
29
+ Project-URL: Documentation, https://docs.blueticks.co
30
+ Project-URL: Source, https://github.com/serenix-com/blueticks-python
31
+ Project-URL: Changelog, https://github.com/serenix-com/blueticks-python/blob/main/CHANGELOG.md
32
+ Keywords: blueticks,whatsapp,api,sdk
33
+ Classifier: Development Status :: 4 - Beta
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
42
+ Requires-Python: >=3.9
43
+ Description-Content-Type: text/markdown
44
+ License-File: LICENSE
45
+ Requires-Dist: httpx<1.0,>=0.27
46
+ Requires-Dist: pydantic<3.0,>=2.6
47
+ Provides-Extra: dev
48
+ Requires-Dist: pytest>=8.0; extra == "dev"
49
+ Requires-Dist: pytest-httpx>=0.30; extra == "dev"
50
+ Requires-Dist: ruff>=0.5; extra == "dev"
51
+ Requires-Dist: mypy>=1.10; extra == "dev"
52
+ Dynamic: license-file
53
+
54
+ # blueticks — Python client for the Blueticks API
55
+
56
+ Official Python SDK for [Blueticks](https://blueticks.co).
57
+
58
+ ## Install
59
+
60
+ ```bash
61
+ pip install blueticks
62
+ ```
63
+
64
+ ## Quickstart
65
+
66
+ ```python
67
+ from blueticks import Blueticks
68
+
69
+ client = Blueticks(api_key="bt_live_...")
70
+ ping = client.ping()
71
+ account = client.account.retrieve()
72
+ print(account.name)
73
+ ```
74
+
75
+ See https://docs.blueticks.co for full documentation.
@@ -0,0 +1,18 @@
1
+ blueticks/__init__.py,sha256=hmmpwdP3y-bD-NHJxeeKWbP7tvBuCE87y5Ye3BHCKs8,522
2
+ blueticks/_base_resource.py,sha256=itU_RAahBEmY91XxGIQoBS0uNIZe5rq797GUyLInNRU,328
3
+ blueticks/_client.py,sha256=-0NeYumrSuQWJkLK6TUO5GxQSi62DB8weo22j5m0A3g,2883
4
+ blueticks/_errors.py,sha256=zSnzxyJNEbWtCgxio9VNosydYwkJ0o7okRkSXRYAD9E,3320
5
+ blueticks/_transport.py,sha256=cbM8pSAZNv2jBfwe8pC0FBFvOVJHHfVxQzr22mkMk6o,4524
6
+ blueticks/_version.py,sha256=J-j-u0itpEFT6irdmWmixQqYMadNl1X91TxUmoiLHMI,22
7
+ blueticks/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ blueticks/resources/__init__.py,sha256=1AtiCuEvYCjHHOup_m9yC8Dbk_RAl0Y6cOUkx1YMEQw,153
9
+ blueticks/resources/account.py,sha256=IXlD5KVbwYX4_wDhtkPRUPmP44oGxw2AmHF7H1gniDA,446
10
+ blueticks/resources/ping.py,sha256=Ger2Kn37nmomnSDVgcpxGoXDqlMaNRl6TBPUGythz28,387
11
+ blueticks/types/__init__.py,sha256=CrPKlrqRuiva695qZR2uU7z_56AL3UG1IxbWdfKFLx8,113
12
+ blueticks/types/account.py,sha256=dMgKf6PhAxA1v8MYbvQgacm93Q615-2tYJy3l_MCGvY,356
13
+ blueticks/types/ping.py,sha256=yUiZBRsa0IvYe4fhURwVM-dS-2pDZNUZsk5IE1Kx1pY,252
14
+ blueticks-1.0.0.dist-info/licenses/LICENSE,sha256=N9gRIkVgfrCtyAj5rMQJcKxcBSbhqmycHvtHbpNWOso,1068
15
+ blueticks-1.0.0.dist-info/METADATA,sha256=d0KHvdg4D-0ATI3Vl945HEiT-4S1Ih0lBqiSFH-eJ4E,2928
16
+ blueticks-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
17
+ blueticks-1.0.0.dist-info/top_level.txt,sha256=Os-3VKE73EIGlrIrw37TpewHTTryFOZRrV2s71v5aPc,10
18
+ blueticks-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 serenix-com
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
+ blueticks