bitculator 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.
bitculator/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+ """Official Python SDK for the Bitculator Data API."""
2
+
3
+ from ._http import Quota
4
+ from ._version import __version__
5
+ from .client import Bitculator
6
+ from .errors import (
7
+ APIConnectionError,
8
+ APIDecodeError,
9
+ APIError,
10
+ APITimeoutError,
11
+ AuthenticationError,
12
+ BitculatorError,
13
+ NotFoundError,
14
+ PermissionDeniedError,
15
+ RateLimitError,
16
+ ServerError,
17
+ ValidationError,
18
+ )
19
+ from .pagination import Page
20
+
21
+ __all__ = [
22
+ "Bitculator",
23
+ "Page",
24
+ "Quota",
25
+ "BitculatorError",
26
+ "APIError",
27
+ "APITimeoutError",
28
+ "APIConnectionError",
29
+ "APIDecodeError",
30
+ "AuthenticationError",
31
+ "PermissionDeniedError",
32
+ "NotFoundError",
33
+ "ValidationError",
34
+ "RateLimitError",
35
+ "ServerError",
36
+ "__version__",
37
+ ]
bitculator/_http.py ADDED
@@ -0,0 +1,214 @@
1
+ """
2
+ The DRY transport core. Every request in the SDK flows through
3
+ ``HttpClient._request``: URL + query building, auth headers, JSON
4
+ (de)serialization, the response envelope, quota capture, retries with backoff,
5
+ timeouts, and error mapping all live here — nowhere else. Uses only the standard
6
+ library (``urllib``) so the SDK has zero runtime dependencies.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import decimal
12
+ import http.client
13
+ import json
14
+ import random
15
+ import time
16
+ import urllib.error
17
+ import urllib.parse
18
+ import urllib.request
19
+ from dataclasses import dataclass
20
+ from typing import Any, Callable, Optional
21
+
22
+ from .errors import APIConnectionError, APIDecodeError, APIError, APITimeoutError, BitculatorError
23
+ from .pagination import Page
24
+
25
+
26
+ @dataclass
27
+ class Quota:
28
+ """Quota headers (``X-Quota-*``) from the most recent response."""
29
+
30
+ limit: Optional[int]
31
+ remaining: Optional[int]
32
+ reset: Optional[int]
33
+ raw: dict
34
+
35
+
36
+ class HttpClient:
37
+ def __init__(
38
+ self,
39
+ *,
40
+ api_key: str,
41
+ base_url: str,
42
+ timeout: float,
43
+ max_retries: int,
44
+ user_agent: str,
45
+ on_quota: Callable[[Quota], None],
46
+ opener: Optional[urllib.request.OpenerDirector] = None,
47
+ ) -> None:
48
+ self._api_key = api_key
49
+ self._base_url = base_url
50
+ self._timeout = timeout
51
+ self._max_retries = max_retries
52
+ self._user_agent = user_agent
53
+ self._on_quota = on_quota
54
+ self._opener = opener or urllib.request.build_opener()
55
+
56
+ def get(self, path: str, params: Optional[dict] = None) -> Any:
57
+ return _unwrap(self._request("GET", path, params=params))
58
+
59
+ def post(self, path: str, body: Any = None, params: Optional[dict] = None) -> Any:
60
+ return _unwrap(self._request("POST", path, params=params, body=body))
61
+
62
+ def delete(self, path: str, params: Optional[dict] = None) -> Any:
63
+ return _unwrap(self._request("DELETE", path, params=params))
64
+
65
+ def get_page(self, path: str, params: Optional[dict] = None) -> Page:
66
+ envelope = self._request("GET", path, params=params)
67
+ return Page(self, path, params or {}, envelope)
68
+
69
+ def _request(self, method: str, path: str, params: Optional[dict] = None, body: Any = None) -> Any:
70
+ url = self._base_url + path + _query(params)
71
+ headers = {
72
+ "Authorization": f"Bearer {self._api_key}",
73
+ "Accept": "application/json",
74
+ "User-Agent": self._user_agent,
75
+ }
76
+ data: Optional[bytes] = None
77
+ if body is not None:
78
+ try:
79
+ data = json.dumps(_snake_keys(body)).encode("utf-8")
80
+ except (TypeError, ValueError) as exc:
81
+ raise BitculatorError(f"Failed to encode request body: {exc}") from exc
82
+ headers["Content-Type"] = "application/json"
83
+
84
+ # Idempotency gate: GET/DELETE are safe to auto-retry; POST is NOT, so a
85
+ # POST the server may already have processed is never re-sent on a 5xx or
86
+ # a network/timeout error. A 429 means the server rejected it before
87
+ # processing, so it stays retryable for every method.
88
+ is_idempotent = method in ("GET", "DELETE")
89
+
90
+ attempt = 0
91
+ while True:
92
+ request = urllib.request.Request(url, data=data, headers=headers, method=method)
93
+ try:
94
+ with self._opener.open(request, timeout=self._timeout) as response:
95
+ self._on_quota(_parse_quota(response.headers))
96
+ raw = response.read()
97
+ if not raw:
98
+ return None
99
+ try:
100
+ return json.loads(raw)
101
+ except (ValueError, TypeError) as exc:
102
+ raise APIDecodeError(f"Failed to decode response from {path}: {exc}") from exc
103
+ except urllib.error.HTTPError as exc:
104
+ self._on_quota(_parse_quota(exc.headers))
105
+ raw = exc.read()
106
+ payload = _safe_json(raw)
107
+ status = exc.code
108
+ retryable = status == 429 or (status >= 500 and is_idempotent)
109
+ if retryable and attempt < self._max_retries:
110
+ time.sleep(_retry_delay(exc.headers, attempt))
111
+ attempt += 1
112
+ continue
113
+ raise APIError.from_response(status, payload, exc.headers) from None
114
+ except (urllib.error.URLError, http.client.HTTPException, TimeoutError, OSError) as exc:
115
+ if is_idempotent and attempt < self._max_retries:
116
+ time.sleep(_backoff(attempt))
117
+ attempt += 1
118
+ continue
119
+ reason = getattr(exc, "reason", exc)
120
+ if isinstance(reason, TimeoutError) or "timed out" in str(reason).lower():
121
+ raise APITimeoutError(f"Request to {path} timed out after {self._timeout}s") from exc
122
+ raise APIConnectionError(f"Network error requesting {path}: {reason}") from exc
123
+
124
+
125
+ # ── helpers (all pure; the single place these concerns are implemented) ──────
126
+
127
+
128
+ def _unwrap(envelope: Any) -> Any:
129
+ """Unwrap the ``{"data": ...}`` envelope; pass through bodies with no data key."""
130
+ if isinstance(envelope, dict) and "data" in envelope:
131
+ return envelope["data"]
132
+ return envelope
133
+
134
+
135
+ def _query(params: Optional[dict]) -> str:
136
+ """Serialize params: drop ``None``, lists→CSV, bools→true/false, trailing ``_`` stripped (``from_``→``from``)."""
137
+ if not params:
138
+ return ""
139
+ pairs = []
140
+ for key, value in params.items():
141
+ if value is None:
142
+ continue
143
+ name = key[:-1] if key.endswith("_") else key
144
+ if isinstance(value, (list, tuple)):
145
+ if not value:
146
+ continue
147
+ pairs.append((name, ",".join(str(item) for item in value)))
148
+ elif isinstance(value, bool):
149
+ pairs.append((name, "true" if value else "false"))
150
+ elif isinstance(value, float):
151
+ # Plain-decimal form (0.0000001, not 1e-07) — keeps the wire format
152
+ # identical across the SDK family.
153
+ pairs.append((name, format(decimal.Decimal(repr(value)), "f")))
154
+ else:
155
+ pairs.append((name, str(value)))
156
+ return "?" + urllib.parse.urlencode(pairs) if pairs else ""
157
+
158
+
159
+ def _snake_keys(body: Any) -> Any:
160
+ """Strip a trailing underscore from body keys (``from_``→``from``); leave the rest as-is."""
161
+ if not isinstance(body, dict):
162
+ return body
163
+ return {(key[:-1] if key.endswith("_") else key): value for key, value in body.items()}
164
+
165
+
166
+ def _parse_quota(headers: Any) -> Quota:
167
+ raw = {}
168
+ if headers is not None:
169
+ for key, value in headers.items():
170
+ lowered = key.lower()
171
+ if lowered.startswith("x-quota-"):
172
+ raw[lowered] = value
173
+
174
+ def num(name: str) -> Optional[int]:
175
+ value = raw.get(name)
176
+ if value in (None, ""):
177
+ return None
178
+ try:
179
+ return int(value)
180
+ except (TypeError, ValueError):
181
+ return None
182
+
183
+ return Quota(
184
+ limit=num("x-quota-limit"),
185
+ remaining=num("x-quota-remaining"),
186
+ reset=num("x-quota-reset"),
187
+ raw=raw,
188
+ )
189
+
190
+
191
+ def _retry_delay(headers: Any, attempt: int) -> float:
192
+ retry_after = headers.get("retry-after") if headers is not None else None
193
+ try:
194
+ seconds = int(retry_after)
195
+ if seconds > 0:
196
+ # Cap at 60s: a misbehaving proxy must not block a synchronous call for an hour.
197
+ return float(min(seconds, 60))
198
+ except (TypeError, ValueError):
199
+ pass
200
+ return _backoff(attempt)
201
+
202
+
203
+ def _backoff(attempt: int) -> float:
204
+ """Exponential backoff with jitter, capped at 8s."""
205
+ return min(8.0, 0.25 * (2 ** attempt)) + random.uniform(0.0, 0.1)
206
+
207
+
208
+ def _safe_json(raw: bytes) -> Any:
209
+ if not raw:
210
+ return None
211
+ try:
212
+ return json.loads(raw)
213
+ except (ValueError, TypeError):
214
+ return None
bitculator/_version.py ADDED
@@ -0,0 +1,3 @@
1
+ """SDK version, sent in the User-Agent header. Single source of truth — pyproject.toml reads it via [tool.hatch.version]."""
2
+
3
+ __version__ = "1.0.0"
bitculator/client.py ADDED
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import urllib.request
4
+ from typing import Optional
5
+
6
+ from ._http import HttpClient, Quota
7
+ from ._version import __version__
8
+ from .errors import BitculatorError
9
+ from .resources import (
10
+ AlarmsResource,
11
+ CalculatorsResource,
12
+ CoinsResource,
13
+ ConversionResource,
14
+ EditorialResource,
15
+ ExchangesResource,
16
+ GlobalMarketResource,
17
+ IndicatorsResource,
18
+ LiquidationsResource,
19
+ MarketsResource,
20
+ MetaResource,
21
+ PricesResource,
22
+ SentimentResource,
23
+ WalletsResource,
24
+ WebhooksResource,
25
+ )
26
+
27
+ DEFAULT_BASE_URL = "https://bitculator.com"
28
+
29
+
30
+ class Bitculator:
31
+ """The Bitculator Data API client.
32
+
33
+ >>> client = Bitculator(api_key="...")
34
+ >>> bitcoin = client.coins.get("bitcoin")
35
+
36
+ Each API group is an attribute on the client: ``client.coins``,
37
+ ``client.prices``, ``client.exchanges``, and so on.
38
+
39
+ Pass ``opener`` (a ``urllib.request.OpenerDirector``) to inject for
40
+ proxies/custom handlers; every request then goes through it.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ api_key: str,
46
+ *,
47
+ base_url: str = DEFAULT_BASE_URL,
48
+ timeout: float = 30.0,
49
+ max_retries: int = 2,
50
+ user_agent: Optional[str] = None,
51
+ opener: Optional[urllib.request.OpenerDirector] = None,
52
+ ) -> None:
53
+ if not api_key:
54
+ raise BitculatorError("A Data API key is required: Bitculator(api_key=...)")
55
+
56
+ self._quota: Optional[Quota] = None
57
+ http = HttpClient(
58
+ api_key=api_key,
59
+ base_url=base_url.rstrip("/"),
60
+ timeout=timeout,
61
+ max_retries=max_retries,
62
+ user_agent=user_agent or f"bitculator-python/{__version__}",
63
+ on_quota=self._set_quota,
64
+ opener=opener,
65
+ )
66
+
67
+ self.coins = CoinsResource(http)
68
+ self.prices = PricesResource(http)
69
+ self.markets = MarketsResource(http)
70
+ self.exchanges = ExchangesResource(http)
71
+ self.wallets = WalletsResource(http)
72
+ self.global_market = GlobalMarketResource(http)
73
+ self.sentiment = SentimentResource(http)
74
+ self.indicators = IndicatorsResource(http)
75
+ self.liquidations = LiquidationsResource(http)
76
+ self.conversion = ConversionResource(http)
77
+ self.calculators = CalculatorsResource(http)
78
+ self.editorial = EditorialResource(http)
79
+ self.alarms = AlarmsResource(http)
80
+ self.webhooks = WebhooksResource(http)
81
+ self.meta = MetaResource(http)
82
+
83
+ def _set_quota(self, quota: Quota) -> None:
84
+ self._quota = quota
85
+
86
+ @property
87
+ def quota(self) -> Optional[Quota]:
88
+ """Quota (``X-Quota-*``) from the most recent response, or ``None`` before the first call."""
89
+ return self._quota
bitculator/errors.py ADDED
@@ -0,0 +1,119 @@
1
+ """
2
+ Exception hierarchy. Everything the SDK raises derives from ``BitculatorError``,
3
+ so a single ``except BitculatorError`` covers it all. HTTP failures raise a
4
+ status-specific ``APIError`` subclass carrying the API's
5
+ ``{"error": {"code", "message", "details"}}`` envelope.
6
+
7
+ (Note: 403 is ``PermissionDeniedError`` and timeouts are ``APITimeoutError`` to
8
+ avoid shadowing the built-in ``PermissionError`` / ``TimeoutError``.)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any, Optional
14
+
15
+
16
+ class BitculatorError(Exception):
17
+ """Base class for every error raised by the SDK."""
18
+
19
+
20
+ class APITimeoutError(BitculatorError):
21
+ """The request exceeded the configured timeout (after retries)."""
22
+
23
+
24
+ class APIConnectionError(BitculatorError):
25
+ """A network-level failure prevented the request from completing."""
26
+
27
+
28
+ class APIDecodeError(BitculatorError):
29
+ """A 2xx body could not be parsed."""
30
+
31
+
32
+ class APIError(BitculatorError):
33
+ """A non-2xx response. Subclasses map to specific status codes."""
34
+
35
+ def __init__(self, status: int, body: Optional[dict], request_id: Optional[str]) -> None:
36
+ err = body or {}
37
+ self.status = status
38
+ self.code: Optional[str] = err.get("code")
39
+ self.details: Any = err.get("details")
40
+ self.request_id = request_id
41
+ self.message: str = err.get("message") or f"HTTP {status}"
42
+ super().__init__(self.message)
43
+
44
+ @staticmethod
45
+ def from_response(status: int, json_body: Any, headers: Any) -> "APIError":
46
+ """Build the most specific error subclass for a failed response."""
47
+ body = _error_body(json_body)
48
+ request_id = headers.get("x-request-id") if headers is not None else None
49
+ if status == 429:
50
+ return RateLimitError(status, body, request_id, _to_int(_header(headers, "retry-after")))
51
+ cls = _STATUS_MAP.get(status)
52
+ if cls is not None:
53
+ return cls(status, body, request_id)
54
+ if status >= 500:
55
+ return ServerError(status, body, request_id)
56
+ return APIError(status, body, request_id)
57
+
58
+
59
+ class AuthenticationError(APIError):
60
+ """401 — missing, malformed, or invalid API key."""
61
+
62
+
63
+ class PermissionDeniedError(APIError):
64
+ """403 — the key's plan does not grant access to this endpoint."""
65
+
66
+
67
+ class NotFoundError(APIError):
68
+ """404 — the requested resource does not exist."""
69
+
70
+
71
+ class ValidationError(APIError):
72
+ """422 — invalid parameters (see ``details``)."""
73
+
74
+
75
+ class ServerError(APIError):
76
+ """5xx — an error on the API side."""
77
+
78
+
79
+ class RateLimitError(APIError):
80
+ """429 — quota or rate limit exceeded."""
81
+
82
+ def __init__(
83
+ self,
84
+ status: int,
85
+ body: Optional[dict],
86
+ request_id: Optional[str],
87
+ retry_after: Optional[int] = None,
88
+ ) -> None:
89
+ super().__init__(status, body, request_id)
90
+ self.retry_after = retry_after
91
+
92
+
93
+ _STATUS_MAP = {
94
+ 401: AuthenticationError,
95
+ 403: PermissionDeniedError,
96
+ 404: NotFoundError,
97
+ 422: ValidationError,
98
+ }
99
+
100
+
101
+ def _error_body(json_body: Any) -> Optional[dict]:
102
+ if isinstance(json_body, dict):
103
+ err = json_body.get("error")
104
+ if isinstance(err, dict):
105
+ return err
106
+ if isinstance(json_body.get("message"), str):
107
+ return {"message": json_body["message"]}
108
+ return None
109
+
110
+
111
+ def _header(headers: Any, name: str) -> Any:
112
+ return headers.get(name) if headers is not None else None
113
+
114
+
115
+ def _to_int(value: Any) -> Optional[int]:
116
+ try:
117
+ return int(value)
118
+ except (TypeError, ValueError):
119
+ return None
@@ -0,0 +1,59 @@
1
+ """
2
+ A single page of a paginated list, plus lazy forward navigation.
3
+
4
+ ``page.data`` is the current page's rows. Iterating a ``Page`` (or calling
5
+ ``auto_paging_iter()``) transparently walks every following page.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, Any, Iterator, List, Optional
11
+
12
+ if TYPE_CHECKING:
13
+ from ._http import HttpClient
14
+
15
+
16
+ class Page:
17
+ def __init__(self, http: "HttpClient", path: str, params: dict, envelope: Any) -> None:
18
+ env = envelope if isinstance(envelope, dict) else {}
19
+ #: Rows on this page.
20
+ self.data: List[Any] = env.get("data", []) or []
21
+ #: Laravel paginator metadata (current_page, last_page, total, ...).
22
+ self.meta: dict = env.get("meta", {}) or {}
23
+ #: Page navigation links.
24
+ self.links: dict = env.get("links", {}) or {}
25
+ self._http = http
26
+ self._path = path
27
+ self._params = params
28
+
29
+ @property
30
+ def has_more(self) -> bool:
31
+ """True if at least one more page exists."""
32
+ if self.links.get("next"):
33
+ return True
34
+ current, last = self.meta.get("current_page"), self.meta.get("last_page")
35
+ return current is not None and last is not None and current < last
36
+
37
+ def next_page(self) -> Optional["Page"]:
38
+ """Fetch the next page, or ``None`` if this is the last one."""
39
+ if not self.has_more:
40
+ return None
41
+ next_number = (self.meta.get("current_page") or 1) + 1
42
+ return self._http.get_page(self._path, {**self._params, "page": next_number})
43
+
44
+ def auto_paging_iter(self) -> Iterator[Any]:
45
+ """Yield every row across this and all following pages, fetching lazily."""
46
+ page: Optional[Page] = self
47
+ while page is not None:
48
+ yield from page.data
49
+ page = page.next_page()
50
+
51
+ def __iter__(self) -> Iterator[Any]:
52
+ """Iterating a page walks all pages (use ``.data`` for just this page)."""
53
+ return self.auto_paging_iter()
54
+
55
+ def __len__(self) -> int:
56
+ return len(self.data)
57
+
58
+ def __repr__(self) -> str:
59
+ return f"<Page rows={len(self.data)} page={self.meta.get('current_page')}/{self.meta.get('last_page')}>"
bitculator/py.typed ADDED
File without changes
@@ -0,0 +1,24 @@
1
+ from .account import AlarmsResource, MetaResource, WebhooksResource
2
+ from .coins import CoinsResource, ConversionResource, PricesResource
3
+ from .editorial import EditorialResource
4
+ from .indicators import CalculatorsResource, IndicatorsResource
5
+ from .market import GlobalMarketResource, LiquidationsResource, SentimentResource
6
+ from .markets import ExchangesResource, MarketsResource, WalletsResource
7
+
8
+ __all__ = [
9
+ "AlarmsResource",
10
+ "CalculatorsResource",
11
+ "CoinsResource",
12
+ "ConversionResource",
13
+ "EditorialResource",
14
+ "ExchangesResource",
15
+ "GlobalMarketResource",
16
+ "IndicatorsResource",
17
+ "LiquidationsResource",
18
+ "MarketsResource",
19
+ "MetaResource",
20
+ "PricesResource",
21
+ "SentimentResource",
22
+ "WalletsResource",
23
+ "WebhooksResource",
24
+ ]
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import urllib.parse
4
+ from typing import Any
5
+
6
+ from .._http import HttpClient
7
+
8
+
9
+ class Resource:
10
+ """Shared base for every resource group: holds the transport and encodes path segments."""
11
+
12
+ def __init__(self, http: HttpClient) -> None:
13
+ self._http = http
14
+
15
+ @staticmethod
16
+ def _enc(value: Any) -> str:
17
+ """Percent-encode a dynamic path segment (slug or id) so it is URL-safe."""
18
+ return urllib.parse.quote(str(value), safe="")
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, List, Union
4
+
5
+ from ..pagination import Page
6
+ from ._base import Resource
7
+
8
+
9
+ class AlarmsResource(Resource):
10
+ """Price alarms owned by the API key."""
11
+
12
+ def list(self, **params: Any) -> Page:
13
+ """List your alarms (paginated). Params: page, per_page, status, direction, notification."""
14
+ return self._http.get_page("/api/v1/alarms", params)
15
+
16
+ def create(
17
+ self,
18
+ *,
19
+ name: str,
20
+ coin: str,
21
+ metric: str,
22
+ direction: str,
23
+ target: float,
24
+ notification: str,
25
+ ) -> Any:
26
+ """Create an alarm."""
27
+ return self._http.post(
28
+ "/api/v1/alarms",
29
+ {
30
+ "name": name,
31
+ "coin": coin,
32
+ "metric": metric,
33
+ "direction": direction,
34
+ "target": target,
35
+ "notification": notification,
36
+ },
37
+ )
38
+
39
+ def delete(self, id: Union[str, int]) -> Any:
40
+ """Delete an alarm by id."""
41
+ return self._http.delete(f"/api/v1/alarms/{self._enc(id)}")
42
+
43
+
44
+ class WebhooksResource(Resource):
45
+ """Webhook endpoints and their delivery log."""
46
+
47
+ def list(self) -> Any:
48
+ """List your webhook endpoints."""
49
+ return self._http.get("/api/v1/webhooks")
50
+
51
+ def create(self, *, url: str, events: List[str]) -> Any:
52
+ """Register a webhook endpoint for a set of events."""
53
+ return self._http.post("/api/v1/webhooks", {"url": url, "events": events})
54
+
55
+ def delete(self, id: Union[str, int]) -> Any:
56
+ """Delete a webhook endpoint by id."""
57
+ return self._http.delete(f"/api/v1/webhooks/{self._enc(id)}")
58
+
59
+ def test(self, id: Union[str, int]) -> Any:
60
+ """Send a test event to a webhook endpoint."""
61
+ return self._http.post(f"/api/v1/webhooks/{self._enc(id)}/test")
62
+
63
+ def deliveries(self, id: Union[str, int], **params: Any) -> Page:
64
+ """Delivery log for a webhook endpoint (paginated). Params: page, per_page."""
65
+ return self._http.get_page(f"/api/v1/webhooks/{self._enc(id)}/deliveries", params)
66
+
67
+
68
+ class MetaResource(Resource):
69
+ """Service metadata: spec, health, and key usage."""
70
+
71
+ def openapi(self) -> Any:
72
+ """The raw OpenAPI specification for the API."""
73
+ return self._http.get("/api/v1/openapi.json")
74
+
75
+ def ping(self) -> Any:
76
+ """Health check."""
77
+ return self._http.get("/api/v1/ping")
78
+
79
+ def usage(self) -> Any:
80
+ """Current key usage and quota."""
81
+ return self._http.get("/api/v1/usage")
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..pagination import Page
6
+ from ._base import Resource
7
+
8
+
9
+ class CoinsResource(Resource):
10
+ """Coins & tokens: listings, movers, detail, and history."""
11
+
12
+ def list(self, **params: Any) -> Page:
13
+ """List coins (paginated). Params: page, per_page, type, status, search,
14
+ min_price, max_price, min_marketcap, max_marketcap, min_volume, max_volume,
15
+ ids, slugs, symbols, sort, interval."""
16
+ return self._http.get_page("/api/v1/coins", params)
17
+
18
+ def recently_added(self, **params: Any) -> Page:
19
+ """Recently added coins (paginated). Same params as :meth:`list`."""
20
+ return self._http.get_page("/api/v1/coins/recently-added", params)
21
+
22
+ def gainers(self, **params: Any) -> Page:
23
+ """Top gainers (paginated). Same params as :meth:`list`."""
24
+ return self._http.get_page("/api/v1/coins/gainers", params)
25
+
26
+ def losers(self, **params: Any) -> Page:
27
+ """Top losers (paginated). Same params as :meth:`list`."""
28
+ return self._http.get_page("/api/v1/coins/losers", params)
29
+
30
+ def trending(self, **params: Any) -> Any:
31
+ """Trending coins. Params: limit."""
32
+ return self._http.get("/api/v1/coins/trending", params)
33
+
34
+ def get(self, slug: str, **params: Any) -> Any:
35
+ """Get a single coin by slug. Params: locale."""
36
+ return self._http.get(f"/api/v1/coins/{self._enc(slug)}", params)
37
+
38
+ def history(self, slug: str, **params: Any) -> Any:
39
+ """OHLC candle history. Params: interval, start, end, limit."""
40
+ return self._http.get(f"/api/v1/coins/{self._enc(slug)}/history", params)
41
+
42
+ def marketcap_history(self, slug: str, **params: Any) -> Any:
43
+ """Marketcap history. Params: interval, start, end, limit."""
44
+ return self._http.get(f"/api/v1/coins/{self._enc(slug)}/marketcap-history", params)
45
+
46
+ def sparkline(self, slug: str, **params: Any) -> Any:
47
+ """Price sparkline. Params: period."""
48
+ return self._http.get(f"/api/v1/coins/{self._enc(slug)}/sparkline", params)
49
+
50
+
51
+ class PricesResource(Resource):
52
+ """Lightweight price lookups."""
53
+
54
+ def list(self, **params: Any) -> Any:
55
+ """Get prices for many coins at once. Params: ids, slugs, symbols, convert."""
56
+ return self._http.get("/api/v1/prices", params)
57
+
58
+ def get(self, slug: str, **params: Any) -> Any:
59
+ """Get the price of a single coin. Params: convert."""
60
+ return self._http.get(f"/api/v1/prices/{self._enc(slug)}", params)
61
+
62
+ def historical(self, **params: Any) -> Any:
63
+ """Historical price of a coin on a date. Params: slug, date."""
64
+ return self._http.get("/api/v1/historical-price", params)
65
+
66
+
67
+ class ConversionResource(Resource):
68
+ """Asset conversion and reference rates."""
69
+
70
+ def convert(self, **params: Any) -> Any:
71
+ """Convert an amount between two assets. Params: from_ (-> ``from``), to, amount."""
72
+ return self._http.get("/api/v1/convert", params)
73
+
74
+ def fiats(self) -> Any:
75
+ """List supported fiat currencies."""
76
+ return self._http.get("/api/v1/fiats")
77
+
78
+ def rates(self) -> Any:
79
+ """List conversion rates."""
80
+ return self._http.get("/api/v1/rates")
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Union
4
+
5
+ from ..pagination import Page
6
+ from ._base import Resource
7
+
8
+
9
+ class EditorialResource(Resource):
10
+ """Editorial content: articles, videos, and insights."""
11
+
12
+ def coin_videos(self, slug: str, **params: Any) -> Page:
13
+ """Videos for a coin (paginated). Params: page, per_page, type, search."""
14
+ return self._http.get_page(f"/api/v1/coins/{self._enc(slug)}/videos", params)
15
+
16
+ def coin_insights(self, slug: str, **params: Any) -> Any:
17
+ """Insight timeline for a coin. Params: locale, offset, limit."""
18
+ return self._http.get(f"/api/v1/coins/{self._enc(slug)}/insights", params)
19
+
20
+ def articles(self, **params: Any) -> Page:
21
+ """List articles (paginated). Params: page, per_page, locale, tag, coin,
22
+ exchange, wallet, search."""
23
+ return self._http.get_page("/api/v1/articles", params)
24
+
25
+ def article(self, slug: str, **params: Any) -> Any:
26
+ """Get a single article by slug. Params: locale."""
27
+ return self._http.get(f"/api/v1/articles/{self._enc(slug)}", params)
28
+
29
+ def submit_article_feedback(self, slug: str, *, helpful: bool) -> Any:
30
+ """Submit helpful/unhelpful feedback on an article."""
31
+ return self._http.post(f"/api/v1/articles/{self._enc(slug)}/feedback", {"helpful": helpful})
32
+
33
+ def video(self, id: Union[str, int]) -> Any:
34
+ """Get a single video by id."""
35
+ return self._http.get(f"/api/v1/videos/{self._enc(id)}")
36
+
37
+ def insights(self, **params: Any) -> Page:
38
+ """List insights (paginated). Params: page, per_page, locale, type, coin, search, sort."""
39
+ return self._http.get_page("/api/v1/insights", params)
40
+
41
+ def insight(self, id: Union[str, int], **params: Any) -> Any:
42
+ """Get a single insight by id. Params: locale."""
43
+ return self._http.get(f"/api/v1/insights/{self._enc(id)}", params)
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ._base import Resource
6
+
7
+
8
+ class IndicatorsResource(Resource):
9
+ """Technical indicators for a coin.
10
+
11
+ Period indicators (rsi, stoch_rsi, sma, cci, mfi, williams_r, price_volatility,
12
+ volume_volatility) accept: period, start, end, limit.
13
+ Series indicators (macd, obv, adx, vwap, cmf) accept: start, end, limit.
14
+ """
15
+
16
+ def _indicator(self, slug: str, name: str, params: dict) -> Any:
17
+ return self._http.get(f"/api/v1/coins/{self._enc(slug)}/indicators/{name}", params)
18
+
19
+ def snapshot(self, slug: str) -> Any:
20
+ """All indicators for a coin at a glance."""
21
+ return self._http.get(f"/api/v1/coins/{self._enc(slug)}/indicators")
22
+
23
+ def rsi(self, slug: str, **params: Any) -> Any:
24
+ """Relative Strength Index."""
25
+ return self._indicator(slug, "rsi", params)
26
+
27
+ def stoch_rsi(self, slug: str, **params: Any) -> Any:
28
+ """Stochastic RSI."""
29
+ return self._indicator(slug, "stoch-rsi", params)
30
+
31
+ def sma(self, slug: str, **params: Any) -> Any:
32
+ """Simple Moving Average."""
33
+ return self._indicator(slug, "sma", params)
34
+
35
+ def cci(self, slug: str, **params: Any) -> Any:
36
+ """Commodity Channel Index."""
37
+ return self._indicator(slug, "cci", params)
38
+
39
+ def mfi(self, slug: str, **params: Any) -> Any:
40
+ """Money Flow Index."""
41
+ return self._indicator(slug, "mfi", params)
42
+
43
+ def williams_r(self, slug: str, **params: Any) -> Any:
44
+ """Williams %R."""
45
+ return self._indicator(slug, "williams-r", params)
46
+
47
+ def price_volatility(self, slug: str, **params: Any) -> Any:
48
+ """Price volatility."""
49
+ return self._indicator(slug, "price-volatility", params)
50
+
51
+ def volume_volatility(self, slug: str, **params: Any) -> Any:
52
+ """Volume volatility."""
53
+ return self._indicator(slug, "volume-volatility", params)
54
+
55
+ def macd(self, slug: str, **params: Any) -> Any:
56
+ """Moving Average Convergence Divergence."""
57
+ return self._indicator(slug, "macd", params)
58
+
59
+ def obv(self, slug: str, **params: Any) -> Any:
60
+ """On-Balance Volume."""
61
+ return self._indicator(slug, "obv", params)
62
+
63
+ def adx(self, slug: str, **params: Any) -> Any:
64
+ """Average Directional Index."""
65
+ return self._indicator(slug, "adx", params)
66
+
67
+ def vwap(self, slug: str, **params: Any) -> Any:
68
+ """Volume-Weighted Average Price."""
69
+ return self._indicator(slug, "vwap", params)
70
+
71
+ def cmf(self, slug: str, **params: Any) -> Any:
72
+ """Chaikin Money Flow."""
73
+ return self._indicator(slug, "cmf", params)
74
+
75
+
76
+ class CalculatorsResource(Resource):
77
+ """Financial calculators."""
78
+
79
+ def dca(self, **params: Any) -> Any:
80
+ """Dollar-cost-averaging calculator. Params: slug, amount, interval, start, end, series."""
81
+ return self._http.get("/api/v1/calculators/dca", params)
82
+
83
+ def profit_loss(self, **params: Any) -> Any:
84
+ """Profit / loss calculator. Params: slug, amount, buy_date, sell_date, buy_fee, sell_fee."""
85
+ return self._http.get("/api/v1/calculators/profit-loss", params)
86
+
87
+ def compound_interest(self, **params: Any) -> Any:
88
+ """Compound interest calculator. Params: principal, rate, duration, duration_unit,
89
+ compound_frequency, contribution, contribution_frequency."""
90
+ return self._http.get("/api/v1/calculators/compound-interest", params)
91
+
92
+ def loan(self, **params: Any) -> Any:
93
+ """Loan-vs-sell calculator. Params: slug, crypto_amount, needed_cash, term_months,
94
+ interest_rate, ltv, expected_growth, tax_rate."""
95
+ return self._http.get("/api/v1/calculators/loan", params)
96
+
97
+ def staking(self, **params: Any) -> Any:
98
+ """Staking rewards calculator. Params: amount, period, period_unit, apy,
99
+ compound_frequency, commission."""
100
+ return self._http.get("/api/v1/calculators/staking", params)
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..pagination import Page
6
+ from ._base import Resource
7
+
8
+
9
+ class GlobalMarketResource(Resource):
10
+ """Global market aggregates."""
11
+
12
+ def snapshot(self) -> Any:
13
+ """Global market snapshot (total marketcap, volume, dominance, ...)."""
14
+ return self._http.get("/api/v1/global")
15
+
16
+ def heatmap(self) -> Any:
17
+ """Market heatmap data."""
18
+ return self._http.get("/api/v1/global/heatmap")
19
+
20
+ def history(self, metric: str, **params: Any) -> Any:
21
+ """Global marketcap or volume history. ``metric`` is e.g. ``marketcap`` or ``volume``. Params: period."""
22
+ return self._http.get(f"/api/v1/global/history/{self._enc(metric)}", params)
23
+
24
+
25
+ class SentimentResource(Resource):
26
+ """Community sentiment and market-wide sentiment indices."""
27
+
28
+ def votes(self, slug: str) -> Any:
29
+ """Community vote tallies for a coin."""
30
+ return self._http.get(f"/api/v1/coins/{self._enc(slug)}/votes")
31
+
32
+ def vote(self, slug: str, *, vote: str) -> Any:
33
+ """Cast a sentiment vote for a coin (``bullish`` or ``bearish``)."""
34
+ return self._http.post(f"/api/v1/coins/{self._enc(slug)}/votes", {"vote": vote})
35
+
36
+ def fear_greed(self, **params: Any) -> Any:
37
+ """Fear & Greed index. Params: coin."""
38
+ return self._http.get("/api/v1/sentiment/fear-greed", params)
39
+
40
+ def bull_bear(self, **params: Any) -> Any:
41
+ """Bull / Bear index. Params: coin."""
42
+ return self._http.get("/api/v1/sentiment/bull-bear", params)
43
+
44
+ def altseason(self, **params: Any) -> Any:
45
+ """Altseason index. Params: days."""
46
+ return self._http.get("/api/v1/sentiment/altseason", params)
47
+
48
+ def indicators(self) -> Any:
49
+ """Aggregate market indicator tally."""
50
+ return self._http.get("/api/v1/sentiment/indicators")
51
+
52
+
53
+ class LiquidationsResource(Resource):
54
+ """Liquidation feed and aggregates."""
55
+
56
+ def feed(self, **params: Any) -> Page:
57
+ """Live liquidation feed (paginated). Params: page, per_page, exchange,
58
+ instrument, position, order, symbol, min_usd."""
59
+ return self._http.get_page("/api/v1/liquidations", params)
60
+
61
+ def hourly(self, **params: Any) -> Any:
62
+ """Hourly liquidation totals. Params: hours."""
63
+ return self._http.get("/api/v1/liquidations/hourly", params)
64
+
65
+ def daily(self, **params: Any) -> Any:
66
+ """Daily liquidation totals. Params: days."""
67
+ return self._http.get("/api/v1/liquidations/daily", params)
68
+
69
+ def summary(self) -> Any:
70
+ """Today's liquidation summary."""
71
+ return self._http.get("/api/v1/liquidations/summary")
72
+
73
+ def netflow(self, **params: Any) -> Any:
74
+ """Long/short liquidation netflow. Params: days."""
75
+ return self._http.get("/api/v1/liquidations/netflow", params)
76
+
77
+ def coins(self, **params: Any) -> Any:
78
+ """Most-liquidated coins. Params: hours, limit."""
79
+ return self._http.get("/api/v1/liquidations/coins", params)
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Union
4
+
5
+ from ..pagination import Page
6
+ from ._base import Resource
7
+
8
+
9
+ class MarketsResource(Resource):
10
+ """Markets, tickers, and trading pairs."""
11
+
12
+ def coin_markets(self, slug: str, **params: Any) -> Page:
13
+ """Markets (tickers) for a coin (paginated). Params: page, per_page, exchange,
14
+ pair, instrument, search, min_volume, max_volume, min_change, max_change, sort."""
15
+ return self._http.get_page(f"/api/v1/coins/{self._enc(slug)}/markets", params)
16
+
17
+ def coin_symbols(self, slug: str) -> Any:
18
+ """Trading symbols for a coin."""
19
+ return self._http.get(f"/api/v1/coins/{self._enc(slug)}/symbols")
20
+
21
+ def tickers(self, **params: Any) -> Page:
22
+ """List tickers across the market (paginated). Same params as :meth:`coin_markets`."""
23
+ return self._http.get_page("/api/v1/tickers", params)
24
+
25
+ def pairs(self, **params: Any) -> Page:
26
+ """List trading pairs (paginated). Params: page, per_page, search, coin,
27
+ min_volume, max_volume, sort."""
28
+ return self._http.get_page("/api/v1/pairs", params)
29
+
30
+ def pair(self, id: Union[str, int]) -> Any:
31
+ """Get a single trading pair by id."""
32
+ return self._http.get(f"/api/v1/pairs/{self._enc(id)}")
33
+
34
+
35
+ class ExchangesResource(Resource):
36
+ """Exchanges: listings, trust score, history, markets, and listed assets."""
37
+
38
+ def list(self, **params: Any) -> Page:
39
+ """List exchanges (paginated). Params: page, per_page, type, search, min_pairs,
40
+ max_pairs, min_assets, max_assets, min_volume, max_volume, ids, slugs, sort."""
41
+ return self._http.get_page("/api/v1/exchanges", params)
42
+
43
+ def get(self, slug: str) -> Any:
44
+ """Get a single exchange by slug."""
45
+ return self._http.get(f"/api/v1/exchanges/{self._enc(slug)}")
46
+
47
+ def trust_score(self, slug: str) -> Any:
48
+ """Get an exchange's trust score."""
49
+ return self._http.get(f"/api/v1/exchanges/{self._enc(slug)}/trust-score")
50
+
51
+ def history(self, slug: str, **params: Any) -> Any:
52
+ """Volume/history for an exchange. Params: interval, start, end, limit."""
53
+ return self._http.get(f"/api/v1/exchanges/{self._enc(slug)}/history", params)
54
+
55
+ def sparkline(self, slug: str, **params: Any) -> Any:
56
+ """Sparkline for an exchange. Params: period."""
57
+ return self._http.get(f"/api/v1/exchanges/{self._enc(slug)}/sparkline", params)
58
+
59
+ def tickers(self, slug: str, **params: Any) -> Page:
60
+ """Markets (tickers) on an exchange (paginated)."""
61
+ return self._http.get_page(f"/api/v1/exchanges/{self._enc(slug)}/tickers", params)
62
+
63
+ def assets(self, slug: str, **params: Any) -> Page:
64
+ """Coins listed on an exchange (paginated)."""
65
+ return self._http.get_page(f"/api/v1/exchanges/{self._enc(slug)}/assets", params)
66
+
67
+
68
+ class WalletsResource(Resource):
69
+ """Wallets: listings, release timeline, comparison, and supported coins."""
70
+
71
+ def list(self, **params: Any) -> Page:
72
+ """List wallets (paginated). Params: page, per_page, search, min_score,
73
+ max_score, tags, ids, slugs, sort."""
74
+ return self._http.get_page("/api/v1/wallets", params)
75
+
76
+ def timeline(self, **params: Any) -> Page:
77
+ """Wallet release timeline (paginated). Same params as :meth:`list`."""
78
+ return self._http.get_page("/api/v1/wallets/timeline", params)
79
+
80
+ def compare(self, **params: Any) -> Any:
81
+ """Compare a set of wallets side by side. Params: slugs."""
82
+ return self._http.get("/api/v1/wallets/compare", params)
83
+
84
+ def get(self, slug: str) -> Any:
85
+ """Get a single wallet by slug."""
86
+ return self._http.get(f"/api/v1/wallets/{self._enc(slug)}")
87
+
88
+ def assets(self, slug: str, **params: Any) -> Page:
89
+ """Coins supported by a wallet (paginated)."""
90
+ return self._http.get_page(f"/api/v1/wallets/{self._enc(slug)}/assets", params)
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: bitculator
3
+ Version: 1.0.0
4
+ Summary: Official Python SDK for the Bitculator Data API
5
+ Project-URL: Documentation, https://bitculator.com/en/documentation/api/v1
6
+ Project-URL: Repository, https://github.com/bitculator/bitculator-python-sdk
7
+ Author: Bitculator
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: api,bitculator,crypto,cryptocurrency,market-data,sdk
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Office/Business :: Financial
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Bitculator Data API — Python SDK
19
+
20
+ [bitculator.com](https://bitculator.com) · [API documentation](https://bitculator.com/en/documentation/api/v1) · [Get an API key](https://bitculator.com/user/developer/api)
21
+ [PyPI — `bitculator`](https://pypi.org/project/bitculator/) · [GitHub](https://github.com/Bitculator/bitculator-python-sdk)
22
+
23
+ The official Python SDK for the [Bitculator Data API](https://bitculator.com).
24
+ Zero dependencies (standard-library only), typed, works on Python 3.9+.
25
+
26
+ > Keep your API key server-side. It is a Bearer token with the `data-api` ability
27
+ > and is not meant for client-side embedding.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install bitculator
33
+ ```
34
+
35
+ ## Quick start
36
+
37
+ ```python
38
+ import os
39
+ from bitculator import Bitculator
40
+
41
+ client = Bitculator(os.environ["BITCULATOR_API_KEY"])
42
+
43
+ # Single resource — the {"data": ...} envelope is unwrapped for you.
44
+ bitcoin = client.coins.get("bitcoin")
45
+ print(bitcoin["price"]) # "63520.780763913" — a decimal string, never rounded
46
+
47
+ # A paginated list.
48
+ page = client.coins.list(per_page=50, sort="-marketcap")
49
+ print(page.data, page.meta["total"])
50
+ ```
51
+
52
+ ## Prices are decimal strings
53
+
54
+ Prices, rates, and supplies come back as **strings** (e.g. `"63520.780763913"`)
55
+ to preserve full precision. Use `decimal.Decimal` if you need to do math — do
56
+ **not** pass them through `float()`, which silently loses precision.
57
+
58
+ ## Pagination
59
+
60
+ Every paginated method returns a `Page`. Read one page, or iterate to walk them all:
61
+
62
+ ```python
63
+ # One page at a time.
64
+ page = client.exchanges.list(per_page=100)
65
+ while page is not None:
66
+ for exchange in page.data:
67
+ print(exchange["name"])
68
+ page = page.next_page()
69
+
70
+ # Or auto-paginate across every page (iterating a Page walks all pages).
71
+ for coin in client.coins.list(sort="-marketcap"):
72
+ print(coin["symbol"])
73
+ ```
74
+
75
+ ## Errors
76
+
77
+ Every failure derives from `BitculatorError`; HTTP failures raise a status-specific
78
+ subclass carrying the `{"error": {"code", "message", "details"}}` envelope.
79
+
80
+ ```python
81
+ from bitculator import ValidationError, RateLimitError, APIError
82
+
83
+ try:
84
+ client.coins.list(per_page=9999) # over the plan cap -> 422
85
+ except ValidationError as err:
86
+ print(err.code, err.details)
87
+ except RateLimitError as err:
88
+ print("retry after", err.retry_after)
89
+ except APIError as err:
90
+ print(err.status, err.message)
91
+ ```
92
+
93
+ | Status | Exception |
94
+ | ------ | --------- |
95
+ | 401 | `AuthenticationError` |
96
+ | 403 | `PermissionDeniedError` (plan does not include this endpoint) |
97
+ | 404 | `NotFoundError` |
98
+ | 422 | `ValidationError` (`.details` holds the field errors) |
99
+ | 429 | `RateLimitError` (`.retry_after`) |
100
+ | 5xx | `ServerError` |
101
+
102
+ Timeouts raise `APITimeoutError`; network failures raise `APIConnectionError`.
103
+
104
+ ## Quota
105
+
106
+ Every response carries `X-Quota-*` headers, surfaced on the client after each call:
107
+
108
+ ```python
109
+ client.coins.list()
110
+ print(client.quota) # Quota(limit=..., remaining=..., reset=..., raw={...})
111
+ ```
112
+
113
+ ## Configuration
114
+
115
+ ```python
116
+ Bitculator(
117
+ api_key, # required (positional)
118
+ base_url="https://bitculator.com", # default
119
+ timeout=30.0, # seconds, default
120
+ max_retries=2, # auto-retry 429 / 5xx / network; 0 to disable
121
+ )
122
+ ```
123
+
124
+ ## Passing parameters
125
+
126
+ Query parameters are keyword arguments and mirror the API's names exactly
127
+ (`per_page`, `min_price`, ...). For the reserved word `from` (in `convert`), pass
128
+ `from_` — the SDK maps it back to `from` on the wire:
129
+
130
+ ```python
131
+ client.conversion.convert(from_="btc", to="usd", amount=1)
132
+ ```
133
+
134
+ ## Resources
135
+
136
+ `coins` · `prices` · `markets` · `exchanges` · `wallets` · `global_market` ·
137
+ `sentiment` · `indicators` · `liquidations` · `conversion` · `calculators` ·
138
+ `editorial` · `alarms` · `webhooks` · `meta`
139
+
140
+ Full endpoint reference: <https://bitculator.com/en/documentation/api/v1>.
141
+
142
+ ## License
143
+
144
+ MIT
@@ -0,0 +1,19 @@
1
+ bitculator/__init__.py,sha256=s5kZCaC2slta55r6fZFoA1XtrW_Ur0zCU7FQghIOOtY,756
2
+ bitculator/_http.py,sha256=su9sQwsoCsOe5Z5knJGjtFYLT2P7Jy6LZ4jzRx1NK4A,8019
3
+ bitculator/_version.py,sha256=Z3eiQCE7mmbeS0xWQqezhPLXUAoEaeLJBVI1TPuNVRU,150
4
+ bitculator/client.py,sha256=2cI3Cw4fdaonrSQK2upU387H_VLa1WYsxY7QSJXsEPA,2760
5
+ bitculator/errors.py,sha256=MGD7VODalgG1EWH3jSffCvdZYHlUZF5u0kCNOOkINlw,3570
6
+ bitculator/pagination.py,sha256=B-x-WefqgmQJE2E_ZkxeDk_C1LDZBskUVKDl94_nAr0,2200
7
+ bitculator/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ bitculator/resources/__init__.py,sha256=rcJwyzPxSTo-hr7UdiAanv6bXgYPskzi7oEn09lt9xA,777
9
+ bitculator/resources/_base.py,sha256=YzA5cqiWfWgmCQc0SPmTdNrygEnFb_BHuurdRfGW9zw,492
10
+ bitculator/resources/account.py,sha256=V5UbbgewXb1icDXuq1U5s9U5GR4fc7gFGs6hfQFdSNY,2570
11
+ bitculator/resources/coins.py,sha256=559KBjhgUO6BCQ1TJLsRIkW-0TZVo4PHpT43jvrUhMw,3224
12
+ bitculator/resources/editorial.py,sha256=TZ3_3CSJcJTrcxoxj5Z84Gb0TU23-agHKA_JuF3y1A0,1902
13
+ bitculator/resources/indicators.py,sha256=01EyAAKguU6S0or96rgY-8Isl48SpRSOyUoGIjx4OIw,3949
14
+ bitculator/resources/market.py,sha256=e4PBKDbrO4g5DBh4d62CUCzA3XrP_KeC1x1U5QRc2EU,3003
15
+ bitculator/resources/markets.py,sha256=xhJgkaRkLPGswA3Tl6JT07rLY1F5JS3c0aRmjIDD_wY,3932
16
+ bitculator-1.0.0.dist-info/METADATA,sha256=3I42OhDqRufhqci2-x8pK48dHPXqe1DIa8S55Ug0IoQ,4481
17
+ bitculator-1.0.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
18
+ bitculator-1.0.0.dist-info/licenses/LICENSE,sha256=IiTpcYvOjmCuzZyrQrcY410bxaA52Cho8PYj6S1sjJA,1067
19
+ bitculator-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bitculator
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.