bigdatacloud 1.0.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.
- bigdatacloud-1.0.0/LICENSE +21 -0
- bigdatacloud-1.0.0/PKG-INFO +167 -0
- bigdatacloud-1.0.0/README.md +139 -0
- bigdatacloud-1.0.0/bigdatacloud/__init__.py +23 -0
- bigdatacloud-1.0.0/bigdatacloud/_deserialise.py +113 -0
- bigdatacloud-1.0.0/bigdatacloud/client.py +334 -0
- bigdatacloud-1.0.0/bigdatacloud/confidence_area.py +65 -0
- bigdatacloud-1.0.0/bigdatacloud/graphql/__init__.py +4 -0
- bigdatacloud-1.0.0/bigdatacloud/graphql/builders.py +185 -0
- bigdatacloud-1.0.0/bigdatacloud/graphql/client.py +197 -0
- bigdatacloud-1.0.0/bigdatacloud/models/__init__.py +39 -0
- bigdatacloud-1.0.0/bigdatacloud/models/common.py +161 -0
- bigdatacloud-1.0.0/bigdatacloud/models/ip_geolocation.py +83 -0
- bigdatacloud-1.0.0/bigdatacloud/models/network.py +220 -0
- bigdatacloud-1.0.0/bigdatacloud/models/reverse_geocoding.py +52 -0
- bigdatacloud-1.0.0/bigdatacloud/models/time_and_location.py +30 -0
- bigdatacloud-1.0.0/bigdatacloud/models/user_agent.py +20 -0
- bigdatacloud-1.0.0/bigdatacloud/models/verification.py +42 -0
- bigdatacloud-1.0.0/bigdatacloud.egg-info/PKG-INFO +167 -0
- bigdatacloud-1.0.0/bigdatacloud.egg-info/SOURCES.txt +23 -0
- bigdatacloud-1.0.0/bigdatacloud.egg-info/dependency_links.txt +1 -0
- bigdatacloud-1.0.0/bigdatacloud.egg-info/requires.txt +6 -0
- bigdatacloud-1.0.0/bigdatacloud.egg-info/top_level.txt +1 -0
- bigdatacloud-1.0.0/pyproject.toml +36 -0
- bigdatacloud-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 BigDataCloud Pty Ltd
|
|
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,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bigdatacloud
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for BigDataCloud APIs — IP Geolocation, Reverse Geocoding, Phone & Email Verification, Network Engineering
|
|
5
|
+
Author-email: BigDataCloud Pty Ltd <support@bigdatacloud.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://www.bigdatacloud.com
|
|
8
|
+
Project-URL: Documentation, https://www.bigdatacloud.com/docs/sdks
|
|
9
|
+
Project-URL: Repository, https://github.com/bigdatacloudapi/bigdatacloud-python
|
|
10
|
+
Keywords: ip-geolocation,reverse-geocoding,bigdatacloud,geolocation,network-engineering
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Topic :: Internet
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: httpx>=0.24
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
26
|
+
Requires-Dist: respx; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# BigDataCloud Python SDK
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/bigdatacloud/)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
|
|
34
|
+
Official Python SDK for [BigDataCloud](https://www.bigdatacloud.com) APIs. Strongly-typed client for IP Geolocation, Reverse Geocoding, Phone & Email Verification, Network Engineering — plus a GraphQL interface for all packages.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install bigdatacloud
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## API Key
|
|
43
|
+
|
|
44
|
+
Get a free API key at [bigdatacloud.com/login](https://www.bigdatacloud.com/login). No credit card required.
|
|
45
|
+
|
|
46
|
+
Store your key in the `BIGDATACLOUD_API_KEY` environment variable:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
export BIGDATACLOUD_API_KEY=your-key-here
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from bigdatacloud import BigDataCloudClient
|
|
56
|
+
|
|
57
|
+
# Reads BIGDATACLOUD_API_KEY from environment
|
|
58
|
+
client = BigDataCloudClient.from_environment()
|
|
59
|
+
|
|
60
|
+
# Or pass the key directly
|
|
61
|
+
# client = BigDataCloudClient("your-key-here")
|
|
62
|
+
|
|
63
|
+
# IP Geolocation
|
|
64
|
+
geo = client.ip_geolocation.get("1.1.1.1")
|
|
65
|
+
print(f"{geo.location.city}, {geo.country.name}")
|
|
66
|
+
|
|
67
|
+
# Full geolocation with hazard report
|
|
68
|
+
full = client.ip_geolocation.get_full("1.1.1.1")
|
|
69
|
+
print(f"Security: {full.security_threat}")
|
|
70
|
+
print(f"Is Tor: {full.hazard_report.is_known_as_tor_server}")
|
|
71
|
+
|
|
72
|
+
# Reverse Geocoding
|
|
73
|
+
place = client.reverse_geocoding.reverse_geocode(-33.87, 151.21)
|
|
74
|
+
print(f"{place.city}, {place.principal_subdivision}, {place.country_name}")
|
|
75
|
+
|
|
76
|
+
# Phone Validation
|
|
77
|
+
phone = client.verification.validate_phone("+61412345678", "AU")
|
|
78
|
+
print(f"Valid: {phone.is_valid}, Type: {phone.line_type}")
|
|
79
|
+
|
|
80
|
+
# Email Verification
|
|
81
|
+
email = client.verification.verify_email("user@example.com")
|
|
82
|
+
print(f"Valid: {email.is_valid}, Disposable: {email.is_disposable}")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Context Manager
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
with BigDataCloudClient.from_environment() as client:
|
|
89
|
+
geo = client.ip_geolocation.get("1.1.1.1")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Confidence Area
|
|
93
|
+
|
|
94
|
+
The `confidence_area` field may encode multiple polygons. Use the helper:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from bigdatacloud import split_into_polygons
|
|
98
|
+
|
|
99
|
+
geo = client.ip_geolocation.get_with_confidence_area("1.1.1.1")
|
|
100
|
+
polygons = split_into_polygons(geo.confidence_area)
|
|
101
|
+
for ring in polygons:
|
|
102
|
+
print(f"Polygon with {len(ring)} points")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## GraphQL
|
|
106
|
+
|
|
107
|
+
BigDataCloud is the only IP geolocation provider with a GraphQL interface. Use the fluent builders to select exactly the fields you need:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# Select only city, country flag, and confidence
|
|
111
|
+
result = client.graphql.ip_geolocation.ip_data("1.1.1.1",
|
|
112
|
+
lambda q: q.locality().country(lambda c: c.flag_emoji()).confidence())
|
|
113
|
+
|
|
114
|
+
print(result["locality"]["city"])
|
|
115
|
+
|
|
116
|
+
# Reverse geocoding with timezone
|
|
117
|
+
loc = client.graphql.reverse_geocoding.location_data(-33.87, 151.21,
|
|
118
|
+
lambda q: q.locality().country().timezone())
|
|
119
|
+
|
|
120
|
+
# Phone & Email
|
|
121
|
+
email = client.graphql.verification.email_verification("user@example.com")
|
|
122
|
+
phone = client.graphql.verification.phone_number("+61412345678")
|
|
123
|
+
|
|
124
|
+
# Network Engineering
|
|
125
|
+
asn = client.graphql.network_engineering.asn_info_full("AS13335",
|
|
126
|
+
lambda q: q.basic_info().receiving_from())
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Raw queries are also supported:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
data = client.graphql.ip_geolocation.query_raw("""
|
|
133
|
+
{
|
|
134
|
+
ipData(ip: "1.1.1.1") {
|
|
135
|
+
locality { city }
|
|
136
|
+
country { name }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
""")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Available APIs
|
|
143
|
+
|
|
144
|
+
| Client | Key Methods |
|
|
145
|
+
|--------|-------------|
|
|
146
|
+
| `client.ip_geolocation` | `get`, `get_with_confidence_area`, `get_full`, `get_country_by_ip`, `get_country_info`, `get_all_countries`, `get_hazard_report`, `get_user_risk`, `get_asn_info`, `get_network_by_ip`, `get_timezone_by_iana_id`, `get_timezone_by_ip`, `parse_user_agent` |
|
|
147
|
+
| `client.reverse_geocoding` | `reverse_geocode`, `reverse_geocode_with_timezone`, `get_timezone_by_location` |
|
|
148
|
+
| `client.verification` | `validate_phone`, `validate_phone_by_ip`, `verify_email` |
|
|
149
|
+
| `client.network_engineering` | `get_asn_info_extended`, `get_receiving_from`, `get_transit_to`, `get_bgp_prefixes`, `get_networks_by_cidr`, `get_asn_rank_list`, `get_tor_exit_nodes` |
|
|
150
|
+
| `client.graphql.ip_geolocation` | `ip_data`, `country_info`, `user_agent`, `timezone_info`, `query_raw` |
|
|
151
|
+
| `client.graphql.reverse_geocoding` | `location_data`, `query_raw` |
|
|
152
|
+
| `client.graphql.verification` | `email_verification`, `phone_number`, `query_raw` |
|
|
153
|
+
| `client.graphql.network_engineering` | `asn_info_full`, `network_by_ip`, `query_raw` |
|
|
154
|
+
|
|
155
|
+
## Samples
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
python samples/ip_geolocation.py
|
|
159
|
+
python samples/reverse_geocoding.py
|
|
160
|
+
python samples/verification.py
|
|
161
|
+
python samples/network_engineering.py
|
|
162
|
+
python samples/graphql_sample.py
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# BigDataCloud Python SDK
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/bigdatacloud/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
Official Python SDK for [BigDataCloud](https://www.bigdatacloud.com) APIs. Strongly-typed client for IP Geolocation, Reverse Geocoding, Phone & Email Verification, Network Engineering — plus a GraphQL interface for all packages.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install bigdatacloud
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## API Key
|
|
15
|
+
|
|
16
|
+
Get a free API key at [bigdatacloud.com/login](https://www.bigdatacloud.com/login). No credit card required.
|
|
17
|
+
|
|
18
|
+
Store your key in the `BIGDATACLOUD_API_KEY` environment variable:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
export BIGDATACLOUD_API_KEY=your-key-here
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from bigdatacloud import BigDataCloudClient
|
|
28
|
+
|
|
29
|
+
# Reads BIGDATACLOUD_API_KEY from environment
|
|
30
|
+
client = BigDataCloudClient.from_environment()
|
|
31
|
+
|
|
32
|
+
# Or pass the key directly
|
|
33
|
+
# client = BigDataCloudClient("your-key-here")
|
|
34
|
+
|
|
35
|
+
# IP Geolocation
|
|
36
|
+
geo = client.ip_geolocation.get("1.1.1.1")
|
|
37
|
+
print(f"{geo.location.city}, {geo.country.name}")
|
|
38
|
+
|
|
39
|
+
# Full geolocation with hazard report
|
|
40
|
+
full = client.ip_geolocation.get_full("1.1.1.1")
|
|
41
|
+
print(f"Security: {full.security_threat}")
|
|
42
|
+
print(f"Is Tor: {full.hazard_report.is_known_as_tor_server}")
|
|
43
|
+
|
|
44
|
+
# Reverse Geocoding
|
|
45
|
+
place = client.reverse_geocoding.reverse_geocode(-33.87, 151.21)
|
|
46
|
+
print(f"{place.city}, {place.principal_subdivision}, {place.country_name}")
|
|
47
|
+
|
|
48
|
+
# Phone Validation
|
|
49
|
+
phone = client.verification.validate_phone("+61412345678", "AU")
|
|
50
|
+
print(f"Valid: {phone.is_valid}, Type: {phone.line_type}")
|
|
51
|
+
|
|
52
|
+
# Email Verification
|
|
53
|
+
email = client.verification.verify_email("user@example.com")
|
|
54
|
+
print(f"Valid: {email.is_valid}, Disposable: {email.is_disposable}")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Context Manager
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
with BigDataCloudClient.from_environment() as client:
|
|
61
|
+
geo = client.ip_geolocation.get("1.1.1.1")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Confidence Area
|
|
65
|
+
|
|
66
|
+
The `confidence_area` field may encode multiple polygons. Use the helper:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from bigdatacloud import split_into_polygons
|
|
70
|
+
|
|
71
|
+
geo = client.ip_geolocation.get_with_confidence_area("1.1.1.1")
|
|
72
|
+
polygons = split_into_polygons(geo.confidence_area)
|
|
73
|
+
for ring in polygons:
|
|
74
|
+
print(f"Polygon with {len(ring)} points")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## GraphQL
|
|
78
|
+
|
|
79
|
+
BigDataCloud is the only IP geolocation provider with a GraphQL interface. Use the fluent builders to select exactly the fields you need:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# Select only city, country flag, and confidence
|
|
83
|
+
result = client.graphql.ip_geolocation.ip_data("1.1.1.1",
|
|
84
|
+
lambda q: q.locality().country(lambda c: c.flag_emoji()).confidence())
|
|
85
|
+
|
|
86
|
+
print(result["locality"]["city"])
|
|
87
|
+
|
|
88
|
+
# Reverse geocoding with timezone
|
|
89
|
+
loc = client.graphql.reverse_geocoding.location_data(-33.87, 151.21,
|
|
90
|
+
lambda q: q.locality().country().timezone())
|
|
91
|
+
|
|
92
|
+
# Phone & Email
|
|
93
|
+
email = client.graphql.verification.email_verification("user@example.com")
|
|
94
|
+
phone = client.graphql.verification.phone_number("+61412345678")
|
|
95
|
+
|
|
96
|
+
# Network Engineering
|
|
97
|
+
asn = client.graphql.network_engineering.asn_info_full("AS13335",
|
|
98
|
+
lambda q: q.basic_info().receiving_from())
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Raw queries are also supported:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
data = client.graphql.ip_geolocation.query_raw("""
|
|
105
|
+
{
|
|
106
|
+
ipData(ip: "1.1.1.1") {
|
|
107
|
+
locality { city }
|
|
108
|
+
country { name }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
""")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Available APIs
|
|
115
|
+
|
|
116
|
+
| Client | Key Methods |
|
|
117
|
+
|--------|-------------|
|
|
118
|
+
| `client.ip_geolocation` | `get`, `get_with_confidence_area`, `get_full`, `get_country_by_ip`, `get_country_info`, `get_all_countries`, `get_hazard_report`, `get_user_risk`, `get_asn_info`, `get_network_by_ip`, `get_timezone_by_iana_id`, `get_timezone_by_ip`, `parse_user_agent` |
|
|
119
|
+
| `client.reverse_geocoding` | `reverse_geocode`, `reverse_geocode_with_timezone`, `get_timezone_by_location` |
|
|
120
|
+
| `client.verification` | `validate_phone`, `validate_phone_by_ip`, `verify_email` |
|
|
121
|
+
| `client.network_engineering` | `get_asn_info_extended`, `get_receiving_from`, `get_transit_to`, `get_bgp_prefixes`, `get_networks_by_cidr`, `get_asn_rank_list`, `get_tor_exit_nodes` |
|
|
122
|
+
| `client.graphql.ip_geolocation` | `ip_data`, `country_info`, `user_agent`, `timezone_info`, `query_raw` |
|
|
123
|
+
| `client.graphql.reverse_geocoding` | `location_data`, `query_raw` |
|
|
124
|
+
| `client.graphql.verification` | `email_verification`, `phone_number`, `query_raw` |
|
|
125
|
+
| `client.graphql.network_engineering` | `asn_info_full`, `network_by_ip`, `query_raw` |
|
|
126
|
+
|
|
127
|
+
## Samples
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
python samples/ip_geolocation.py
|
|
131
|
+
python samples/reverse_geocoding.py
|
|
132
|
+
python samples/verification.py
|
|
133
|
+
python samples/network_engineering.py
|
|
134
|
+
python samples/graphql_sample.py
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BigDataCloud Python SDK
|
|
3
|
+
Official client for BigDataCloud APIs.
|
|
4
|
+
|
|
5
|
+
Quick start::
|
|
6
|
+
|
|
7
|
+
from bigdatacloud import BigDataCloudClient
|
|
8
|
+
|
|
9
|
+
client = BigDataCloudClient.from_environment()
|
|
10
|
+
geo = client.ip_geolocation.get("1.1.1.1")
|
|
11
|
+
print(f"{geo.location.city}, {geo.country.name}")
|
|
12
|
+
"""
|
|
13
|
+
from .client import BigDataCloudClient, BigDataCloudException
|
|
14
|
+
from .confidence_area import split_into_polygons, is_multi_polygon
|
|
15
|
+
from .models import *
|
|
16
|
+
|
|
17
|
+
__version__ = "1.0.0"
|
|
18
|
+
__all__ = [
|
|
19
|
+
"BigDataCloudClient",
|
|
20
|
+
"BigDataCloudException",
|
|
21
|
+
"split_into_polygons",
|
|
22
|
+
"is_multi_polygon",
|
|
23
|
+
]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lightweight JSON → dataclass deserialiser.
|
|
3
|
+
Converts snake_case and camelCase field names automatically.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any, Dict, List, Optional, Type, TypeVar, get_type_hints
|
|
8
|
+
import dataclasses
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _to_snake(name: str) -> str:
|
|
14
|
+
"""Convert camelCase or PascalCase to snake_case."""
|
|
15
|
+
s1 = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name)
|
|
16
|
+
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_origin(tp: Any) -> Any:
|
|
20
|
+
return getattr(tp, "__origin__", None)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_args(tp: Any) -> tuple:
|
|
24
|
+
return getattr(tp, "__args__", ())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def from_dict(cls: Type[T], data: Optional[Dict[str, Any]]) -> Optional[T]:
|
|
28
|
+
"""Deserialise a dict into a dataclass instance, handling nested types."""
|
|
29
|
+
if data is None:
|
|
30
|
+
return None
|
|
31
|
+
if not isinstance(data, dict):
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
if not dataclasses.is_dataclass(cls):
|
|
35
|
+
return data # type: ignore
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
hints = get_type_hints(cls)
|
|
39
|
+
except Exception:
|
|
40
|
+
# Fall back to field annotations if get_type_hints fails (forward refs etc.)
|
|
41
|
+
hints = {f.name: f.type for f in dataclasses.fields(cls)} # type: ignore
|
|
42
|
+
kwargs: Dict[str, Any] = {}
|
|
43
|
+
|
|
44
|
+
for f in dataclasses.fields(cls): # type: ignore
|
|
45
|
+
field_name = f.name # snake_case field name
|
|
46
|
+
# Try snake_case first, then camelCase variants
|
|
47
|
+
camel = re.sub(r"_([a-z])", lambda m: m.group(1).upper(), field_name)
|
|
48
|
+
# Special: i_cloud → iCloud
|
|
49
|
+
camel2 = re.sub(r"_([A-Z])", lambda m: m.group(1), camel)
|
|
50
|
+
|
|
51
|
+
raw = data.get(field_name) or data.get(camel) or data.get(camel2)
|
|
52
|
+
|
|
53
|
+
if raw is None:
|
|
54
|
+
# Use the default
|
|
55
|
+
kwargs[field_name] = f.default if f.default is not dataclasses.MISSING else (
|
|
56
|
+
f.default_factory() if f.default_factory is not dataclasses.MISSING else None # type: ignore
|
|
57
|
+
)
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
field_type = hints.get(field_name)
|
|
61
|
+
kwargs[field_name] = _convert(raw, field_type)
|
|
62
|
+
|
|
63
|
+
return cls(**kwargs) # type: ignore
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _convert(value: Any, tp: Any) -> Any:
|
|
67
|
+
"""Recursively convert value to the target type."""
|
|
68
|
+
if tp is None or value is None:
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
origin = _get_origin(tp)
|
|
72
|
+
args = _get_args(tp)
|
|
73
|
+
|
|
74
|
+
# Optional[X] → unwrap
|
|
75
|
+
if origin is type(None):
|
|
76
|
+
return None
|
|
77
|
+
if str(origin) in ("<class 'typing.Union'>", "typing.Union") or (
|
|
78
|
+
hasattr(tp, "__origin__") and tp.__origin__ is type(None)
|
|
79
|
+
):
|
|
80
|
+
# Handle Optional properly
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
# Check for Union (Optional is Union[X, None])
|
|
84
|
+
import typing
|
|
85
|
+
if hasattr(typing, "get_args"):
|
|
86
|
+
actual_args = typing.get_args(tp)
|
|
87
|
+
actual_origin = typing.get_origin(tp)
|
|
88
|
+
if actual_origin is typing.Union:
|
|
89
|
+
# Pick the non-None type
|
|
90
|
+
non_none = [a for a in actual_args if a is not type(None)]
|
|
91
|
+
if non_none:
|
|
92
|
+
return _convert(value, non_none[0])
|
|
93
|
+
return value
|
|
94
|
+
|
|
95
|
+
# List[X]
|
|
96
|
+
if origin is list:
|
|
97
|
+
item_type = args[0] if args else None
|
|
98
|
+
if isinstance(value, list):
|
|
99
|
+
return [_convert(item, item_type) for item in value]
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
# Dataclass
|
|
103
|
+
if dataclasses.is_dataclass(tp) and isinstance(value, dict):
|
|
104
|
+
return from_dict(tp, value)
|
|
105
|
+
|
|
106
|
+
# Primitives
|
|
107
|
+
if tp in (int, float, str, bool) and value is not None:
|
|
108
|
+
try:
|
|
109
|
+
return tp(value)
|
|
110
|
+
except (ValueError, TypeError):
|
|
111
|
+
return value
|
|
112
|
+
|
|
113
|
+
return value
|