py-cloudip 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.
- py_cloudip-0.1.0/.gitignore +12 -0
- py_cloudip-0.1.0/LICENSE +21 -0
- py_cloudip-0.1.0/PKG-INFO +129 -0
- py_cloudip-0.1.0/README.md +110 -0
- py_cloudip-0.1.0/pyproject.toml +37 -0
- py_cloudip-0.1.0/src/cloudip/__init__.py +166 -0
- py_cloudip-0.1.0/src/cloudip/__main__.py +4 -0
- py_cloudip-0.1.0/src/cloudip/cache.py +75 -0
- py_cloudip-0.1.0/src/cloudip/cli.py +105 -0
- py_cloudip-0.1.0/src/cloudip/constants.py +15 -0
- py_cloudip-0.1.0/src/cloudip/data/cloudip.msgpack.gz +0 -0
- py_cloudip-0.1.0/src/cloudip/decode.py +44 -0
- py_cloudip-0.1.0/src/cloudip/detector.py +296 -0
- py_cloudip-0.1.0/src/cloudip/embedded.py +90 -0
- py_cloudip-0.1.0/src/cloudip/embedded_loader.py +28 -0
- py_cloudip-0.1.0/src/cloudip/source.py +56 -0
- py_cloudip-0.1.0/src/cloudip/trie.py +79 -0
- py_cloudip-0.1.0/src/cloudip/types.py +126 -0
py_cloudip-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rez Moss
|
|
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,129 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py-cloudip
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fast cloud-provider IP detection (AWS, GCP, Azure, Cloudflare, DigitalOcean, Oracle)
|
|
5
|
+
Project-URL: Homepage, https://github.com/rezmoss/py-cloudip
|
|
6
|
+
Project-URL: Database, https://github.com/rezmoss/cloudip-db
|
|
7
|
+
Author: Rez Moss
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: aws,azure,cidr,cloud,cloudflare,gcp,geoip,ip
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Internet
|
|
15
|
+
Classifier: Topic :: System :: Networking
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Requires-Dist: msgpack>=1.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# py-cloudip
|
|
21
|
+
|
|
22
|
+
Fast cloud-provider IP detection for Python. Identify whether an IP address
|
|
23
|
+
belongs to **AWS, GCP, Azure, Cloudflare, DigitalOcean, or Oracle Cloud** via
|
|
24
|
+
longest-prefix-match lookups over a Patricia/binary trie.
|
|
25
|
+
|
|
26
|
+
A Python port of [js-cloudip](https://github.com/rezmoss/js-cloudip) and
|
|
27
|
+
[go-cloudip](https://github.com/rezmoss/go-cloudip), backed by the daily-updated
|
|
28
|
+
[cloudip-db](https://github.com/rezmoss/cloudip-db) MessagePack database.
|
|
29
|
+
|
|
30
|
+
- **Fast** — binary trie, IPv4 + IPv6, longest-prefix match.
|
|
31
|
+
- **Auto-updating** — fetches fresh data from `cloudip-db` with SHA-256 verification.
|
|
32
|
+
- **Offline-capable** — file cache plus an embedded database bundled in the wheel.
|
|
33
|
+
- **Zero config** — works on first call; one tiny dependency (`msgpack`).
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install py-cloudip
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import cloudip
|
|
45
|
+
|
|
46
|
+
cloudip.is_aws("52.94.76.1") # True
|
|
47
|
+
cloudip.get_provider("34.64.0.1") # "gcp"
|
|
48
|
+
cloudip.is_cloud_provider("1.1.1.1") # True
|
|
49
|
+
|
|
50
|
+
r = cloudip.lookup("52.94.76.1")
|
|
51
|
+
# LookupResult(found=True, provider="aws", region="us-east-1", service="EC2",
|
|
52
|
+
# cidr="52.94.76.0/22", ip_type="ipv4")
|
|
53
|
+
r.to_dict()
|
|
54
|
+
# {"found": True, "provider": "aws", "cidr": "52.94.76.0/22",
|
|
55
|
+
# "ip_type": "ipv4", "region": "us-east-1", "service": "EC2"}
|
|
56
|
+
|
|
57
|
+
# Forward lookup: every CIDR for one or more providers
|
|
58
|
+
cloudip.get_ips("cloudflare") # list[IPEntry]
|
|
59
|
+
cloudip.get_ips(["aws", "gcp"])
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Provider checks
|
|
63
|
+
|
|
64
|
+
`is_aws`, `is_gcp`, `is_azure`, `is_cloudflare`, `is_digitalocean`, `is_oracle`,
|
|
65
|
+
`is_cloud_provider`.
|
|
66
|
+
|
|
67
|
+
### Metadata & updates
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
cloudip.version() # "2026-06-05"
|
|
71
|
+
cloudip.range_count() # 124455
|
|
72
|
+
cloudip.providers() # ["aws", "gcp", "cloudflare", "azure", "digitalocean", "oracle"]
|
|
73
|
+
cloudip.check_update() # CheckUpdateResult(has_update=..., info=VersionInfo(...))
|
|
74
|
+
cloudip.update() # force refresh
|
|
75
|
+
cloudip.clear_cache()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Custom detector
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
detector = cloudip.new_detector(
|
|
82
|
+
data_dir="./cache", # None disables file caching; "" = default ~/.cache/py-cloudip
|
|
83
|
+
auto_update_seconds=86400, # background refresh (min 1h); 0 disables
|
|
84
|
+
offline=False, # air-gapped mode
|
|
85
|
+
verify_sha256=True,
|
|
86
|
+
ttl_seconds=86400,
|
|
87
|
+
)
|
|
88
|
+
detector.lookup("52.94.76.1")
|
|
89
|
+
detector.close() # stop the background updater
|
|
90
|
+
# or: with cloudip.Detector(...) as d: ...
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Offline / air-gapped
|
|
94
|
+
|
|
95
|
+
The `cloudip.embedded` module never touches the network — it uses only the
|
|
96
|
+
database bundled in the package:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from cloudip import embedded
|
|
100
|
+
embedded.is_aws("52.94.76.1") # uses bundled data only
|
|
101
|
+
embedded.age_days() # how old the bundled data is
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## CLI
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
cloudip lookup 52.94.76.1
|
|
108
|
+
cloudip provider 34.64.0.1
|
|
109
|
+
cloudip get cloudflare
|
|
110
|
+
cloudip get aws,gcp
|
|
111
|
+
cloudip providers
|
|
112
|
+
cloudip version
|
|
113
|
+
cloudip check-update
|
|
114
|
+
cloudip update
|
|
115
|
+
cloudip clear-cache
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
(Also runnable as `python -m cloudip`.)
|
|
119
|
+
|
|
120
|
+
## How it works
|
|
121
|
+
|
|
122
|
+
1. Fetch `version.json` + `cloudip.msgpack.gz` from `cloudip-db`.
|
|
123
|
+
2. Verify the SHA-256 of the decompressed MessagePack against `version.json`.
|
|
124
|
+
3. Decode and build per-protocol tries for sub-millisecond lookups.
|
|
125
|
+
4. On network failure, fall back to the on-disk cache, then the embedded database.
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# py-cloudip
|
|
2
|
+
|
|
3
|
+
Fast cloud-provider IP detection for Python. Identify whether an IP address
|
|
4
|
+
belongs to **AWS, GCP, Azure, Cloudflare, DigitalOcean, or Oracle Cloud** via
|
|
5
|
+
longest-prefix-match lookups over a Patricia/binary trie.
|
|
6
|
+
|
|
7
|
+
A Python port of [js-cloudip](https://github.com/rezmoss/js-cloudip) and
|
|
8
|
+
[go-cloudip](https://github.com/rezmoss/go-cloudip), backed by the daily-updated
|
|
9
|
+
[cloudip-db](https://github.com/rezmoss/cloudip-db) MessagePack database.
|
|
10
|
+
|
|
11
|
+
- **Fast** — binary trie, IPv4 + IPv6, longest-prefix match.
|
|
12
|
+
- **Auto-updating** — fetches fresh data from `cloudip-db` with SHA-256 verification.
|
|
13
|
+
- **Offline-capable** — file cache plus an embedded database bundled in the wheel.
|
|
14
|
+
- **Zero config** — works on first call; one tiny dependency (`msgpack`).
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install py-cloudip
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import cloudip
|
|
26
|
+
|
|
27
|
+
cloudip.is_aws("52.94.76.1") # True
|
|
28
|
+
cloudip.get_provider("34.64.0.1") # "gcp"
|
|
29
|
+
cloudip.is_cloud_provider("1.1.1.1") # True
|
|
30
|
+
|
|
31
|
+
r = cloudip.lookup("52.94.76.1")
|
|
32
|
+
# LookupResult(found=True, provider="aws", region="us-east-1", service="EC2",
|
|
33
|
+
# cidr="52.94.76.0/22", ip_type="ipv4")
|
|
34
|
+
r.to_dict()
|
|
35
|
+
# {"found": True, "provider": "aws", "cidr": "52.94.76.0/22",
|
|
36
|
+
# "ip_type": "ipv4", "region": "us-east-1", "service": "EC2"}
|
|
37
|
+
|
|
38
|
+
# Forward lookup: every CIDR for one or more providers
|
|
39
|
+
cloudip.get_ips("cloudflare") # list[IPEntry]
|
|
40
|
+
cloudip.get_ips(["aws", "gcp"])
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Provider checks
|
|
44
|
+
|
|
45
|
+
`is_aws`, `is_gcp`, `is_azure`, `is_cloudflare`, `is_digitalocean`, `is_oracle`,
|
|
46
|
+
`is_cloud_provider`.
|
|
47
|
+
|
|
48
|
+
### Metadata & updates
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
cloudip.version() # "2026-06-05"
|
|
52
|
+
cloudip.range_count() # 124455
|
|
53
|
+
cloudip.providers() # ["aws", "gcp", "cloudflare", "azure", "digitalocean", "oracle"]
|
|
54
|
+
cloudip.check_update() # CheckUpdateResult(has_update=..., info=VersionInfo(...))
|
|
55
|
+
cloudip.update() # force refresh
|
|
56
|
+
cloudip.clear_cache()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Custom detector
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
detector = cloudip.new_detector(
|
|
63
|
+
data_dir="./cache", # None disables file caching; "" = default ~/.cache/py-cloudip
|
|
64
|
+
auto_update_seconds=86400, # background refresh (min 1h); 0 disables
|
|
65
|
+
offline=False, # air-gapped mode
|
|
66
|
+
verify_sha256=True,
|
|
67
|
+
ttl_seconds=86400,
|
|
68
|
+
)
|
|
69
|
+
detector.lookup("52.94.76.1")
|
|
70
|
+
detector.close() # stop the background updater
|
|
71
|
+
# or: with cloudip.Detector(...) as d: ...
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Offline / air-gapped
|
|
75
|
+
|
|
76
|
+
The `cloudip.embedded` module never touches the network — it uses only the
|
|
77
|
+
database bundled in the package:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from cloudip import embedded
|
|
81
|
+
embedded.is_aws("52.94.76.1") # uses bundled data only
|
|
82
|
+
embedded.age_days() # how old the bundled data is
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## CLI
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
cloudip lookup 52.94.76.1
|
|
89
|
+
cloudip provider 34.64.0.1
|
|
90
|
+
cloudip get cloudflare
|
|
91
|
+
cloudip get aws,gcp
|
|
92
|
+
cloudip providers
|
|
93
|
+
cloudip version
|
|
94
|
+
cloudip check-update
|
|
95
|
+
cloudip update
|
|
96
|
+
cloudip clear-cache
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
(Also runnable as `python -m cloudip`.)
|
|
100
|
+
|
|
101
|
+
## How it works
|
|
102
|
+
|
|
103
|
+
1. Fetch `version.json` + `cloudip.msgpack.gz` from `cloudip-db`.
|
|
104
|
+
2. Verify the SHA-256 of the decompressed MessagePack against `version.json`.
|
|
105
|
+
3. Decode and build per-protocol tries for sub-millisecond lookups.
|
|
106
|
+
4. On network failure, fall back to the on-disk cache, then the embedded database.
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "py-cloudip"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Fast cloud-provider IP detection (AWS, GCP, Azure, Cloudflare, DigitalOcean, Oracle)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Rez Moss" }]
|
|
13
|
+
keywords = ["cloud", "ip", "aws", "gcp", "azure", "cloudflare", "cidr", "geoip"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Internet",
|
|
19
|
+
"Topic :: System :: Networking",
|
|
20
|
+
]
|
|
21
|
+
dependencies = ["msgpack>=1.0"]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/rezmoss/py-cloudip"
|
|
25
|
+
Database = "https://github.com/rezmoss/cloudip-db"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
cloudip = "cloudip.cli:main"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["src/cloudip"]
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
34
|
+
"src/cloudip/data/cloudip.msgpack.gz" = "cloudip/data/cloudip.msgpack.gz"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.sdist]
|
|
37
|
+
include = ["src/cloudip", "README.md", "LICENSE"]
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""py-cloudip — fast cloud-provider IP detection.
|
|
2
|
+
|
|
3
|
+
Detect whether an IP address belongs to AWS, GCP, Azure, Cloudflare,
|
|
4
|
+
DigitalOcean, or Oracle Cloud. Data comes from the rezmoss/cloudip-db database
|
|
5
|
+
(network fetch with SHA-256 verification, on-disk cache, and an embedded
|
|
6
|
+
offline fallback).
|
|
7
|
+
|
|
8
|
+
Quick start::
|
|
9
|
+
|
|
10
|
+
import cloudip
|
|
11
|
+
cloudip.is_aws("52.94.76.1") # True
|
|
12
|
+
cloudip.get_provider("34.64.0.1") # "gcp"
|
|
13
|
+
cloudip.lookup("52.94.76.1") # LookupResult(found=True, provider="aws", ...)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import threading
|
|
19
|
+
from typing import List, Optional, Union
|
|
20
|
+
|
|
21
|
+
from .constants import (
|
|
22
|
+
PROVIDER_AWS,
|
|
23
|
+
PROVIDER_AZURE,
|
|
24
|
+
PROVIDER_CLOUDFLARE,
|
|
25
|
+
PROVIDER_DIGITALOCEAN,
|
|
26
|
+
PROVIDER_GCP,
|
|
27
|
+
PROVIDER_ORACLE,
|
|
28
|
+
)
|
|
29
|
+
from .detector import Detector, load_version, new_detector
|
|
30
|
+
from .types import (
|
|
31
|
+
CheckUpdateResult,
|
|
32
|
+
Database,
|
|
33
|
+
IPEntry,
|
|
34
|
+
LookupResult,
|
|
35
|
+
Provider,
|
|
36
|
+
Range,
|
|
37
|
+
VersionInfo,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__version__ = "0.1.0"
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"Detector",
|
|
44
|
+
"new_detector",
|
|
45
|
+
"lookup",
|
|
46
|
+
"get_provider",
|
|
47
|
+
"is_cloud_provider",
|
|
48
|
+
"is_aws",
|
|
49
|
+
"is_gcp",
|
|
50
|
+
"is_azure",
|
|
51
|
+
"is_cloudflare",
|
|
52
|
+
"is_digitalocean",
|
|
53
|
+
"is_oracle",
|
|
54
|
+
"get_ips",
|
|
55
|
+
"version",
|
|
56
|
+
"range_count",
|
|
57
|
+
"providers",
|
|
58
|
+
"update",
|
|
59
|
+
"check_update",
|
|
60
|
+
"clear_cache",
|
|
61
|
+
"remote_version",
|
|
62
|
+
"CheckUpdateResult",
|
|
63
|
+
"Database",
|
|
64
|
+
"IPEntry",
|
|
65
|
+
"LookupResult",
|
|
66
|
+
"Provider",
|
|
67
|
+
"Range",
|
|
68
|
+
"VersionInfo",
|
|
69
|
+
"PROVIDER_AWS",
|
|
70
|
+
"PROVIDER_GCP",
|
|
71
|
+
"PROVIDER_AZURE",
|
|
72
|
+
"PROVIDER_CLOUDFLARE",
|
|
73
|
+
"PROVIDER_DIGITALOCEAN",
|
|
74
|
+
"PROVIDER_ORACLE",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
_default: Optional[Detector] = None
|
|
78
|
+
_default_lock = threading.Lock()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _get_default() -> Detector:
|
|
82
|
+
global _default
|
|
83
|
+
if _default is not None:
|
|
84
|
+
return _default
|
|
85
|
+
with _default_lock:
|
|
86
|
+
if _default is None:
|
|
87
|
+
_default = new_detector()
|
|
88
|
+
return _default
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def lookup(ip: str) -> LookupResult:
|
|
92
|
+
return _get_default().lookup(ip)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_provider(ip: str) -> Provider:
|
|
96
|
+
return _get_default().get_provider(ip)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def is_cloud_provider(ip: str) -> bool:
|
|
100
|
+
return _get_default().is_cloud_provider(ip)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def is_aws(ip: str) -> bool:
|
|
104
|
+
return _get_default().is_aws(ip)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def is_gcp(ip: str) -> bool:
|
|
108
|
+
return _get_default().is_gcp(ip)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def is_azure(ip: str) -> bool:
|
|
112
|
+
return _get_default().is_azure(ip)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def is_cloudflare(ip: str) -> bool:
|
|
116
|
+
return _get_default().is_cloudflare(ip)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def is_digitalocean(ip: str) -> bool:
|
|
120
|
+
return _get_default().is_digitalocean(ip)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def is_oracle(ip: str) -> bool:
|
|
124
|
+
return _get_default().is_oracle(ip)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_ips(
|
|
128
|
+
providers: Optional[Union[Provider, List[Provider]]] = None
|
|
129
|
+
) -> List[IPEntry]:
|
|
130
|
+
return _get_default().get_ips(providers)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def version() -> str:
|
|
134
|
+
return _get_default().version()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def range_count() -> int:
|
|
138
|
+
return _get_default().range_count()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def providers() -> List[Provider]:
|
|
142
|
+
return _get_default().providers()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def update() -> None:
|
|
146
|
+
_get_default().update()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def check_update() -> CheckUpdateResult:
|
|
150
|
+
return _get_default().check_update()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def remote_version(version_url: Optional[str] = None) -> VersionInfo:
|
|
154
|
+
if version_url is None:
|
|
155
|
+
from .constants import DEFAULT_VERSION_URL
|
|
156
|
+
|
|
157
|
+
version_url = DEFAULT_VERSION_URL
|
|
158
|
+
return load_version(version_url)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def clear_cache() -> None:
|
|
162
|
+
global _default
|
|
163
|
+
if _default is not None:
|
|
164
|
+
_default.clear_cache()
|
|
165
|
+
_default.close()
|
|
166
|
+
_default = None
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""On-disk caching of the downloaded database under a user cache directory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import NamedTuple, Optional
|
|
11
|
+
|
|
12
|
+
_DATA_FILE = "cloudip.msgpack.gz"
|
|
13
|
+
_META_FILE = "version.json"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CachedData(NamedTuple):
|
|
17
|
+
version: str
|
|
18
|
+
bytes: bytes
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _default_cache_dir() -> str:
|
|
22
|
+
base = (
|
|
23
|
+
os.environ.get("XDG_CACHE_HOME")
|
|
24
|
+
or os.path.join(os.path.expanduser("~"), ".cache")
|
|
25
|
+
)
|
|
26
|
+
return os.path.join(base, "py-cloudip")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def resolve_cache_dir(dir: Optional[str]) -> Optional[str]:
|
|
30
|
+
"""``None`` disables caching; an unset (sentinel) value uses the default dir."""
|
|
31
|
+
if dir is None:
|
|
32
|
+
return None
|
|
33
|
+
if dir == "":
|
|
34
|
+
return _default_cache_dir()
|
|
35
|
+
return dir
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def read_cache(dir: str) -> Optional[CachedData]:
|
|
39
|
+
try:
|
|
40
|
+
p = Path(dir)
|
|
41
|
+
data = (p / _DATA_FILE).read_bytes()
|
|
42
|
+
meta = json.loads((p / _META_FILE).read_text("utf-8"))
|
|
43
|
+
return CachedData(version=meta.get("version", ""), bytes=data)
|
|
44
|
+
except (OSError, ValueError):
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def write_cache(dir: str, version: str, data: bytes) -> None:
|
|
49
|
+
try:
|
|
50
|
+
p = Path(dir)
|
|
51
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
(p / _DATA_FILE).write_bytes(data)
|
|
53
|
+
(p / _META_FILE).write_text(
|
|
54
|
+
json.dumps({"version": version, "stored_at": time.time()})
|
|
55
|
+
)
|
|
56
|
+
except OSError:
|
|
57
|
+
pass # best-effort
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def cache_age_seconds(dir: str) -> Optional[float]:
|
|
61
|
+
try:
|
|
62
|
+
meta = json.loads((Path(dir) / _META_FILE).read_text("utf-8"))
|
|
63
|
+
return time.time() - float(meta["stored_at"])
|
|
64
|
+
except (OSError, ValueError, KeyError):
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def clear_cache(dir: Optional[str]) -> None:
|
|
69
|
+
resolved = resolve_cache_dir(dir if dir is not None else "")
|
|
70
|
+
if not resolved:
|
|
71
|
+
return
|
|
72
|
+
try:
|
|
73
|
+
shutil.rmtree(resolved, ignore_errors=True)
|
|
74
|
+
except OSError:
|
|
75
|
+
pass
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Command-line interface: ``cloudip <command> [args]``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
from . import (
|
|
10
|
+
check_update,
|
|
11
|
+
clear_cache,
|
|
12
|
+
get_ips,
|
|
13
|
+
get_provider,
|
|
14
|
+
lookup,
|
|
15
|
+
providers,
|
|
16
|
+
range_count,
|
|
17
|
+
update,
|
|
18
|
+
version,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
HELP = """cloudip — cloud provider IP utilities (py-cloudip)
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
cloudip lookup <ip> Reverse-lookup an IP address
|
|
25
|
+
cloudip get <provider>[,...] Print CIDRs for one or more providers
|
|
26
|
+
cloudip provider <ip> Print provider name for an IP
|
|
27
|
+
cloudip providers List supported providers
|
|
28
|
+
cloudip version Print local data version + range count
|
|
29
|
+
cloudip check-update Check if a newer upstream version exists
|
|
30
|
+
cloudip update Force a refresh from cloudip-db
|
|
31
|
+
cloudip clear-cache Delete the local cache
|
|
32
|
+
cloudip help Show this help
|
|
33
|
+
|
|
34
|
+
Data source: rezmoss/cloudip-db
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main(argv: List[str] = None) -> int:
|
|
39
|
+
argv = sys.argv[1:] if argv is None else argv
|
|
40
|
+
cmd = argv[0] if argv else None
|
|
41
|
+
args = argv[1:]
|
|
42
|
+
|
|
43
|
+
if cmd in (None, "help", "-h", "--help"):
|
|
44
|
+
sys.stdout.write(HELP)
|
|
45
|
+
return 0
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
if cmd == "lookup":
|
|
49
|
+
if not args:
|
|
50
|
+
raise ValueError("usage: cloudip lookup <ip>")
|
|
51
|
+
r = lookup(args[0])
|
|
52
|
+
print(json.dumps(r.to_dict(), indent=2))
|
|
53
|
+
return 0 if r.found else 1
|
|
54
|
+
|
|
55
|
+
if cmd == "provider":
|
|
56
|
+
if not args:
|
|
57
|
+
raise ValueError("usage: cloudip provider <ip>")
|
|
58
|
+
p = get_provider(args[0])
|
|
59
|
+
if not p:
|
|
60
|
+
print("unknown")
|
|
61
|
+
return 1
|
|
62
|
+
print(p)
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
if cmd == "get":
|
|
66
|
+
if not args:
|
|
67
|
+
raise ValueError("usage: cloudip get <provider>[,<provider>]")
|
|
68
|
+
want = [s.strip() for s in args[0].split(",") if s.strip()]
|
|
69
|
+
for e in get_ips(want):
|
|
70
|
+
print(e.ip_address)
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
if cmd == "providers":
|
|
74
|
+
for p in providers():
|
|
75
|
+
print(p)
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
if cmd == "version":
|
|
79
|
+
print(f"{version()} ({range_count()} ranges)")
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
if cmd == "check-update":
|
|
83
|
+
result = check_update()
|
|
84
|
+
print(json.dumps(result.to_dict(), indent=2))
|
|
85
|
+
return 0 if result.has_update else 1
|
|
86
|
+
|
|
87
|
+
if cmd == "update":
|
|
88
|
+
update()
|
|
89
|
+
print("updated")
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
if cmd == "clear-cache":
|
|
93
|
+
clear_cache()
|
|
94
|
+
print("cache cleared")
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
sys.stderr.write(f"unknown command: {cmd}\n{HELP}")
|
|
98
|
+
return 2
|
|
99
|
+
except Exception as err: # noqa: BLE001
|
|
100
|
+
sys.stderr.write(f"error: {err}\n")
|
|
101
|
+
return 1
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Shared constants: provider names, default URLs, and timing defaults."""
|
|
2
|
+
|
|
3
|
+
PROVIDER_AWS = "aws"
|
|
4
|
+
PROVIDER_GCP = "gcp"
|
|
5
|
+
PROVIDER_AZURE = "azure"
|
|
6
|
+
PROVIDER_CLOUDFLARE = "cloudflare"
|
|
7
|
+
PROVIDER_DIGITALOCEAN = "digitalocean"
|
|
8
|
+
PROVIDER_ORACLE = "oracle"
|
|
9
|
+
|
|
10
|
+
DEFAULT_BASE_URL = "https://raw.githubusercontent.com/rezmoss/cloudip-db/main/data"
|
|
11
|
+
DEFAULT_DATA_URL = f"{DEFAULT_BASE_URL}/cloudip.msgpack.gz"
|
|
12
|
+
DEFAULT_VERSION_URL = f"{DEFAULT_BASE_URL}/version.json"
|
|
13
|
+
|
|
14
|
+
HOUR_SECONDS = 60 * 60
|
|
15
|
+
DEFAULT_TTL_SECONDS = 24 * HOUR_SECONDS
|
|
Binary file
|