ipclues 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.
ipclues/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ from .client import Client
2
+ from .database import Database
3
+ from .exceptions import IPCluesError
4
+ from .response import APIResponse, APIResponseData, RateLimitMetadata, ResponseMetadata
5
+
6
+ __all__ = [
7
+ "APIResponse",
8
+ "APIResponseData",
9
+ "Client",
10
+ "Database",
11
+ "IPCluesError",
12
+ "RateLimitMetadata",
13
+ "ResponseMetadata",
14
+ ]
ipclues/client.py ADDED
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from urllib.error import HTTPError, URLError
4
+ from urllib.parse import quote, urlencode
5
+ from urllib.request import Request, urlopen
6
+
7
+ from .database import Database
8
+ from .exceptions import IPCluesError
9
+ from .response import APIResponse, APIResponseData, make_response
10
+
11
+
12
+ class Client:
13
+ def __init__(
14
+ self,
15
+ api_key: str,
16
+ *,
17
+ base_url: str = "https://api.ipclues.com/v1",
18
+ timeout: float = 10.0,
19
+ ) -> None:
20
+ if not api_key.strip():
21
+ raise ValueError("IPClues API key is required")
22
+ if timeout <= 0:
23
+ raise ValueError("Timeout must be a positive number")
24
+
25
+ self._api_key = api_key
26
+ self._base_url = base_url.rstrip("/")
27
+ self._timeout = timeout
28
+
29
+ def lookup(
30
+ self,
31
+ ip: str,
32
+ *,
33
+ db: Database | list[Database] | tuple[Database, ...] | None = None,
34
+ ) -> APIResponse[APIResponseData]:
35
+ if not ip.strip():
36
+ raise ValueError("IP address is required")
37
+
38
+ query = ""
39
+ if db is not None:
40
+ databases = [db] if isinstance(db, Database) else list(db)
41
+ if not databases:
42
+ raise ValueError("At least one database must be selected")
43
+ if not all(isinstance(item, Database) for item in databases):
44
+ raise ValueError("Every database must be a Database enum")
45
+ query = "?" + urlencode({"db": ",".join(item.value for item in databases)})
46
+
47
+ url = f"{self._base_url}/lookup/ip/{quote(ip, safe='')}{query}"
48
+ request = Request(
49
+ url,
50
+ headers={
51
+ "Authorization": f"Bearer {self._api_key}",
52
+ "Accept": "application/json",
53
+ },
54
+ )
55
+
56
+ try:
57
+ with urlopen(request, timeout=self._timeout) as response:
58
+ return make_response(
59
+ response.status,
60
+ getattr(response, "reason", ""),
61
+ response.headers,
62
+ response.read(),
63
+ )
64
+ except HTTPError as error:
65
+ # HTTPError contains the response body and headers; return the envelope.
66
+ try:
67
+ return make_response(
68
+ error.code, error.reason, error.headers, error.read()
69
+ )
70
+ finally:
71
+ error.close()
72
+ except URLError as error:
73
+ raise IPCluesError(f"IPClues request failed: {error.reason}") from error
ipclues/database.py ADDED
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Database(str, Enum):
5
+ GEO_I3_LITE = "geoi3-lite"
6
+ GEO_I3_COUNTRY = "geoi3-country"
7
+ NET_I3_IDENTITY = "neti3-identity"
ipclues/exceptions.py ADDED
@@ -0,0 +1,2 @@
1
+ class IPCluesError(Exception):
2
+ """Base exception for IPClues SDK request failures."""
ipclues/response.py ADDED
@@ -0,0 +1,116 @@
1
+ import json
2
+ import re
3
+ from dataclasses import dataclass
4
+ from http import HTTPStatus
5
+ from typing import Any, Generic, TypeVar
6
+
7
+ T = TypeVar("T")
8
+ APIResponseData = dict[str, Any]
9
+
10
+
11
+ def integer(value: str | None) -> int | None:
12
+ if value is None:
13
+ return None
14
+ try:
15
+ return int(value)
16
+ except ValueError:
17
+ return None
18
+
19
+
20
+ def database_versions(value: str | None) -> dict[str, str] | None:
21
+ if not value:
22
+ return None
23
+ result: dict[str, str] = {}
24
+ for item in value.split(","):
25
+ key, separator, version = item.partition("=")
26
+ if separator and key.strip() and version.strip():
27
+ result[key.strip()] = version.strip()
28
+ return result or None
29
+
30
+
31
+ def decode_body(raw: bytes) -> Any:
32
+ text = raw.decode("utf-8", errors="replace")
33
+ if not text:
34
+ return None
35
+ try:
36
+ return json.loads(text)
37
+ except json.JSONDecodeError:
38
+ return text
39
+
40
+
41
+ def make_response(
42
+ status: int, phrase: str, headers: Any, raw: bytes
43
+ ) -> "APIResponse"[Any]:
44
+ try:
45
+ reason = HTTPStatus(status).phrase
46
+ except ValueError:
47
+ reason = phrase or ""
48
+
49
+ databases = headers.get("I3-Databases")
50
+ metadata = ResponseMetadata(
51
+ status=status,
52
+ reason=reason,
53
+ request_id=headers.get("I3-Request-Id"),
54
+ credits_charged=integer(headers.get("I3-Credits-Charged")),
55
+ credits_remaining=integer(headers.get("I3-Credits-Remaining")),
56
+ databases=(
57
+ [item.strip() for item in databases.split(",") if item.strip()]
58
+ if databases
59
+ else None
60
+ ),
61
+ database_versions=database_versions(headers.get("I3-Database-Versions")),
62
+ rate_limit=rate_limit(
63
+ headers.get("Ratelimit"),
64
+ headers.get("Ratelimit-Policy"),
65
+ headers.get("Retry-After"),
66
+ ),
67
+ )
68
+ return APIResponse(data=decode_body(raw), metadata=metadata)
69
+
70
+
71
+ def rate_limit(
72
+ value: str | None, policy_value: str | None, retry_value: str | None
73
+ ) -> "RateLimitMetadata":
74
+ def parameter(source: str | None, name: str) -> int | None:
75
+ match = re.search(rf"(?:^|;)\s*{name}=(\d+)", source or "")
76
+ return integer(match.group(1)) if match else None
77
+
78
+ match = re.match(r'^"?([^";]+)"?', value or "")
79
+ policy_match = re.match(r'^"?([^";]+)"?', policy_value or "")
80
+
81
+ return RateLimitMetadata(
82
+ policy=(
83
+ match.group(1) if match else policy_match.group(1) if policy_match else None
84
+ ),
85
+ limit=parameter(value, "q"),
86
+ remaining=parameter(value, "r"),
87
+ reset=parameter(value, "t"),
88
+ retry_after=integer(retry_value),
89
+ )
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class APIResponse(Generic[T]):
94
+ data: T
95
+ metadata: "ResponseMetadata"
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class RateLimitMetadata:
100
+ policy: str | None = None
101
+ limit: int | None = None
102
+ remaining: int | None = None
103
+ reset: int | None = None
104
+ retry_after: int | None = None
105
+
106
+
107
+ @dataclass(frozen=True)
108
+ class ResponseMetadata:
109
+ status: int
110
+ reason: str
111
+ request_id: str | None = None
112
+ credits_charged: int | None = None
113
+ credits_remaining: int | None = None
114
+ databases: list[str] | None = None
115
+ database_versions: dict[str, str] | None = None
116
+ rate_limit: RateLimitMetadata | None = None
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: ipclues
3
+ Version: 0.1.0
4
+ Summary: Official IPClues SDK for Python
5
+ Keywords: ip,geolocation,network-intelligence,ipclues,api
6
+ Author: Rufai Limantawa
7
+ Author-email: Rufai Limantawa <rufailimantawa@gmail.com>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Programming Language :: Python :: 3.15
19
+ Classifier: Typing :: Typed
20
+ Requires-Dist: ruff>=0.17.3 ; extra == 'dev'
21
+ Requires-Dist: twine>=7.0 ; extra == 'dev'
22
+ Requires-Python: >=3.10
23
+ Project-URL: Homepage, https://ipclues.com
24
+ Project-URL: Repository, https://github.com/ipclues/python-sdk
25
+ Project-URL: Issues, https://github.com/ipclues/python-sdk/issues
26
+ Provides-Extra: dev
27
+ Description-Content-Type: text/markdown
28
+
29
+ # IPClues Python SDK
30
+
31
+ Official Python SDK for the [IPClues](https://ipclues.com) IP intelligence API.
32
+
33
+ ## Requirements
34
+
35
+ - Python 3.10+
36
+ - An IPClues API key
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ python -m pip install ipclues
42
+ ```
43
+
44
+ ## Quick start
45
+
46
+ ```python
47
+ import os
48
+ from ipclues import Client, Database
49
+
50
+ client = Client("YOUR_API_KEY")
51
+
52
+ response = client.lookup(
53
+ "1.1.1.1",
54
+ db=[Database.GEO_I3_LITE, Database.NET_I3_IDENTITY],
55
+ )
56
+
57
+ print(response.data)
58
+ print(response.metadata.status)
59
+ print(response.metadata.credits_remaining)
60
+ print(response.metadata.database_versions)
61
+ ```
62
+
63
+ ## Response behavior
64
+
65
+ The SDK wraps the API response body and HTTP metadata:
66
+
67
+ - `response.data`: original decoded response body.
68
+ - `response.metadata.status`: HTTP status code.
69
+ - `response.metadata.request_id`: `I3-Request-Id` header.
70
+ - `response.metadata.credits_charged` and `credits_remaining`: credit usage headers.
71
+ - `response.metadata.databases` and `database_versions`: response database headers.
72
+ - `response.metadata.rate_limit`: parsed rate-limit headers.
73
+
74
+ HTTP error responses such as 401, 402, and 429 are returned as `APIResponse` objects too. Network failures and timeouts raise exceptions.
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,8 @@
1
+ ipclues/__init__.py,sha256=HNXblYk-lDDTKdOOjjU0DAtyYKCg8xbLftCz9G_a8tk,339
2
+ ipclues/client.py,sha256=-BJZvCP0IXfDx_ViA_VhpkTC74L7H2T6kqOTGxFCJlo,2505
3
+ ipclues/database.py,sha256=NcoBybyzb3C2nO3qu53RVVEHFQyB6KqzlSbP9bLif9E,158
4
+ ipclues/exceptions.py,sha256=T7j6xXjghp2_a9hd4ogmjFZDmnBImhM-OoZxGdbDyaE,90
5
+ ipclues/response.py,sha256=dD1P3Gw6S6bJ3cRuujdq3MRhNNVUXXIS-beLyT3dNbY,3295
6
+ ipclues-0.1.0.dist-info/WHEEL,sha256=3NO1oYsF7mANFUWVdyEQLM-zmecczWWEmmF5rEbXrWk,80
7
+ ipclues-0.1.0.dist-info/METADATA,sha256=fNbqX-3VMFB7p7d46ngqGYP9kP3YMIlr9aAbHHg-GTA,2302
8
+ ipclues-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.13.6
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any