ipclues 0.1.0__tar.gz
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-0.1.0/PKG-INFO +78 -0
- ipclues-0.1.0/README.md +50 -0
- ipclues-0.1.0/pyproject.toml +53 -0
- ipclues-0.1.0/pyproject.toml.orig +41 -0
- ipclues-0.1.0/src/ipclues/__init__.py +14 -0
- ipclues-0.1.0/src/ipclues/client.py +73 -0
- ipclues-0.1.0/src/ipclues/database.py +7 -0
- ipclues-0.1.0/src/ipclues/exceptions.py +2 -0
- ipclues-0.1.0/src/ipclues/response.py +116 -0
ipclues-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
ipclues-0.1.0/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# IPClues Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [IPClues](https://ipclues.com) IP intelligence API.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Python 3.10+
|
|
8
|
+
- An IPClues API key
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
python -m pip install ipclues
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import os
|
|
20
|
+
from ipclues import Client, Database
|
|
21
|
+
|
|
22
|
+
client = Client("YOUR_API_KEY")
|
|
23
|
+
|
|
24
|
+
response = client.lookup(
|
|
25
|
+
"1.1.1.1",
|
|
26
|
+
db=[Database.GEO_I3_LITE, Database.NET_I3_IDENTITY],
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
print(response.data)
|
|
30
|
+
print(response.metadata.status)
|
|
31
|
+
print(response.metadata.credits_remaining)
|
|
32
|
+
print(response.metadata.database_versions)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Response behavior
|
|
36
|
+
|
|
37
|
+
The SDK wraps the API response body and HTTP metadata:
|
|
38
|
+
|
|
39
|
+
- `response.data`: original decoded response body.
|
|
40
|
+
- `response.metadata.status`: HTTP status code.
|
|
41
|
+
- `response.metadata.request_id`: `I3-Request-Id` header.
|
|
42
|
+
- `response.metadata.credits_charged` and `credits_remaining`: credit usage headers.
|
|
43
|
+
- `response.metadata.databases` and `database_versions`: response database headers.
|
|
44
|
+
- `response.metadata.rate_limit`: parsed rate-limit headers.
|
|
45
|
+
|
|
46
|
+
HTTP error responses such as 401, 402, and 429 are returned as `APIResponse` objects too. Network failures and timeouts raise exceptions.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.13.6,<0.14"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ipclues"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
description = "Official IPClues SDK for Python"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
keywords = [
|
|
13
|
+
"ip",
|
|
14
|
+
"geolocation",
|
|
15
|
+
"network-intelligence",
|
|
16
|
+
"ipclues",
|
|
17
|
+
"api",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Programming Language :: Python :: 3.14",
|
|
29
|
+
"Programming Language :: Python :: 3.15",
|
|
30
|
+
"Typing :: Typed",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[[project.authors]]
|
|
34
|
+
name = "Rufai Limantawa"
|
|
35
|
+
email = "rufailimantawa@gmail.com"
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = [
|
|
39
|
+
"ruff>=0.17.3",
|
|
40
|
+
"twine>=7.0",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://ipclues.com"
|
|
45
|
+
Repository = "https://github.com/ipclues/python-sdk"
|
|
46
|
+
Issues = "https://github.com/ipclues/python-sdk/issues"
|
|
47
|
+
|
|
48
|
+
[tool.uv]
|
|
49
|
+
package = true
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 88
|
|
53
|
+
target-version = "py310"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.13.6,<0.14"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ipclues"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
authors = [{ name = "Rufai Limantawa", email = "rufailimantawa@gmail.com" }]
|
|
10
|
+
description = "Official IPClues SDK for Python"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
license = "MIT"
|
|
13
|
+
keywords = ["ip", "geolocation", "network-intelligence", "ipclues", "api"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Programming Language :: Python :: 3.15",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = ["ruff>=0.17.3", "twine>=7.0"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://ipclues.com"
|
|
33
|
+
Repository = "https://github.com/ipclues/python-sdk"
|
|
34
|
+
Issues = "https://github.com/ipclues/python-sdk/issues"
|
|
35
|
+
|
|
36
|
+
[tool.uv]
|
|
37
|
+
package = true
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
line-length = 88
|
|
41
|
+
target-version = "py310"
|
|
@@ -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
|
+
]
|
|
@@ -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
|
|
@@ -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
|