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.
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .pytest_cache/
10
+ .mypy_cache/
11
+ cache/
12
+ *.swp
@@ -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,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -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