python-missive 0.2.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.
- pymissive/__init__.py +3 -0
- pymissive/__main__.py +9 -0
- pymissive/archives/__init__.py +142 -0
- pymissive/archives/__main__.py +9 -0
- pymissive/archives/address.py +272 -0
- pymissive/archives/address_backends/__init__.py +29 -0
- pymissive/archives/address_backends/base.py +610 -0
- pymissive/archives/address_backends/geoapify.py +221 -0
- pymissive/archives/address_backends/geocode_earth.py +210 -0
- pymissive/archives/address_backends/google_maps.py +371 -0
- pymissive/archives/address_backends/here.py +348 -0
- pymissive/archives/address_backends/locationiq.py +271 -0
- pymissive/archives/address_backends/mapbox.py +314 -0
- pymissive/archives/address_backends/maps_co.py +257 -0
- pymissive/archives/address_backends/nominatim.py +348 -0
- pymissive/archives/address_backends/opencage.py +292 -0
- pymissive/archives/address_backends/pelias_mixin.py +181 -0
- pymissive/archives/address_backends/photon.py +322 -0
- pymissive/archives/cli.py +42 -0
- pymissive/archives/helpers.py +45 -0
- pymissive/archives/missive.py +64 -0
- pymissive/archives/providers/__init__.py +167 -0
- pymissive/archives/providers/apn.py +171 -0
- pymissive/archives/providers/ar24.py +204 -0
- pymissive/archives/providers/base/__init__.py +203 -0
- pymissive/archives/providers/base/_attachments.py +166 -0
- pymissive/archives/providers/base/branded.py +341 -0
- pymissive/archives/providers/base/common.py +781 -0
- pymissive/archives/providers/base/email.py +422 -0
- pymissive/archives/providers/base/email_message.py +85 -0
- pymissive/archives/providers/base/monitoring.py +150 -0
- pymissive/archives/providers/base/notification.py +187 -0
- pymissive/archives/providers/base/postal.py +742 -0
- pymissive/archives/providers/base/postal_defaults.py +26 -0
- pymissive/archives/providers/base/sms.py +213 -0
- pymissive/archives/providers/base/voice_call.py +82 -0
- pymissive/archives/providers/brevo.py +363 -0
- pymissive/archives/providers/certeurope.py +249 -0
- pymissive/archives/providers/django_email.py +182 -0
- pymissive/archives/providers/fcm.py +91 -0
- pymissive/archives/providers/laposte.py +392 -0
- pymissive/archives/providers/maileva.py +511 -0
- pymissive/archives/providers/mailgun.py +118 -0
- pymissive/archives/providers/messenger.py +74 -0
- pymissive/archives/providers/notification.py +112 -0
- pymissive/archives/providers/sendgrid.py +160 -0
- pymissive/archives/providers/ses.py +185 -0
- pymissive/archives/providers/signal.py +68 -0
- pymissive/archives/providers/slack.py +80 -0
- pymissive/archives/providers/smtp.py +190 -0
- pymissive/archives/providers/teams.py +91 -0
- pymissive/archives/providers/telegram.py +69 -0
- pymissive/archives/providers/twilio.py +310 -0
- pymissive/archives/providers/vonage.py +208 -0
- pymissive/archives/sender.py +339 -0
- pymissive/archives/status.py +22 -0
- pymissive/cli.py +42 -0
- pymissive/config.py +397 -0
- pymissive/helpers.py +0 -0
- pymissive/providers/apn.py +8 -0
- pymissive/providers/ar24.py +8 -0
- pymissive/providers/base/__init__.py +64 -0
- pymissive/providers/base/acknowledgement.py +6 -0
- pymissive/providers/base/attachments.py +10 -0
- pymissive/providers/base/branded.py +16 -0
- pymissive/providers/base/email.py +2 -0
- pymissive/providers/base/notification.py +2 -0
- pymissive/providers/base/postal.py +2 -0
- pymissive/providers/base/sms.py +2 -0
- pymissive/providers/base/voice_call.py +2 -0
- pymissive/providers/brevo.py +420 -0
- pymissive/providers/certeurope.py +8 -0
- pymissive/providers/django_email.py +8 -0
- pymissive/providers/fcm.py +8 -0
- pymissive/providers/laposte.py +8 -0
- pymissive/providers/maileva.py +8 -0
- pymissive/providers/mailgun.py +8 -0
- pymissive/providers/messenger.py +8 -0
- pymissive/providers/notification.py +8 -0
- pymissive/providers/partner.py +650 -0
- pymissive/providers/scaleway.py +498 -0
- pymissive/providers/sendgrid.py +8 -0
- pymissive/providers/ses.py +8 -0
- pymissive/providers/signal.py +8 -0
- pymissive/providers/slack.py +8 -0
- pymissive/providers/smtp.py +8 -0
- pymissive/providers/teams.py +8 -0
- pymissive/providers/telegram.py +8 -0
- pymissive/providers/twilio.py +8 -0
- pymissive/providers/vonage.py +8 -0
- python_missive-0.2.0.dist-info/METADATA +152 -0
- python_missive-0.2.0.dist-info/RECORD +95 -0
- python_missive-0.2.0.dist-info/WHEEL +5 -0
- python_missive-0.2.0.dist-info/entry_points.txt +2 -0
- python_missive-0.2.0.dist-info/top_level.txt +1 -0
pymissive/__init__.py
ADDED
pymissive/__main__.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Lightweight multi-channel messaging helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional, Sequence, Union
|
|
6
|
+
|
|
7
|
+
from .address import Address
|
|
8
|
+
from .address_backends import (BaseAddressBackend, GoogleMapsAddressBackend,
|
|
9
|
+
HereAddressBackend, MapboxAddressBackend,
|
|
10
|
+
NominatimAddressBackend, PhotonAddressBackend)
|
|
11
|
+
from .helpers import (
|
|
12
|
+
DEFAULT_MIN_ADDRESS_CONFIDENCE,
|
|
13
|
+
describe_address_backends,
|
|
14
|
+
format_phone_international,
|
|
15
|
+
get_address_backend_by_attribute,
|
|
16
|
+
get_address_backends_from_config,
|
|
17
|
+
get_address_by_reference,
|
|
18
|
+
get_provider_by_attribute,
|
|
19
|
+
search_addresses,
|
|
20
|
+
)
|
|
21
|
+
from .missive import Missive
|
|
22
|
+
from .providers.base.common import BaseProviderCommon
|
|
23
|
+
from .sender import MissiveSender
|
|
24
|
+
from .status import MissiveStatus
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"MissiveStatus",
|
|
28
|
+
"BaseProviderCommon",
|
|
29
|
+
"Address",
|
|
30
|
+
"Missive",
|
|
31
|
+
"MissiveSender",
|
|
32
|
+
"send_missive",
|
|
33
|
+
"format_phone_international",
|
|
34
|
+
"get_address_backends_from_config",
|
|
35
|
+
"get_address_by_reference",
|
|
36
|
+
"search_addresses",
|
|
37
|
+
"get_address_backend_by_attribute",
|
|
38
|
+
"describe_address_backends",
|
|
39
|
+
"get_provider_by_attribute",
|
|
40
|
+
"DEFAULT_MIN_ADDRESS_CONFIDENCE",
|
|
41
|
+
"BaseAddressBackend",
|
|
42
|
+
"GoogleMapsAddressBackend",
|
|
43
|
+
"HereAddressBackend",
|
|
44
|
+
"MapboxAddressBackend",
|
|
45
|
+
"NominatimAddressBackend",
|
|
46
|
+
"PhotonAddressBackend",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def send_missive(
|
|
51
|
+
missive_type: str,
|
|
52
|
+
body: str,
|
|
53
|
+
subject: Optional[str] = None,
|
|
54
|
+
recipient_email: Optional[str] = None,
|
|
55
|
+
recipient_phone: Optional[str] = None,
|
|
56
|
+
recipient: Optional[Any] = None,
|
|
57
|
+
providers_config: Optional[Union[Sequence[str], Dict[str, Dict[str, Any]]]] = None,
|
|
58
|
+
config: Optional[Dict[str, Any]] = None,
|
|
59
|
+
sandbox: bool = False,
|
|
60
|
+
enable_fallback: bool = True,
|
|
61
|
+
**kwargs: Any,
|
|
62
|
+
) -> Missive:
|
|
63
|
+
"""Send a missive with automatic provider selection and fallback.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
missive_type: Type of missive (EMAIL, SMS, POSTAL, etc.)
|
|
67
|
+
body: Message body/content
|
|
68
|
+
subject: Message subject (required for EMAIL)
|
|
69
|
+
recipient_email: Recipient email address (for EMAIL)
|
|
70
|
+
recipient_phone: Recipient phone number (for SMS, VOICE_CALL)
|
|
71
|
+
recipient: Complex recipient object with metadata (for PUSH_NOTIFICATION, etc.)
|
|
72
|
+
providers_config: Either:
|
|
73
|
+
- List of provider import paths: ["pymissive.providers.brevo.BrevoProvider"]
|
|
74
|
+
- Dict mapping paths to configs: {"path": {"API_KEY": "value"}}
|
|
75
|
+
config: Default configuration dict merged with provider-specific configs
|
|
76
|
+
sandbox: If True, forces sandbox mode for all providers (no real sends)
|
|
77
|
+
enable_fallback: If True, try next provider on failure
|
|
78
|
+
**kwargs: Additional options (provider_options, is_registered, etc.)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Missive object with updated status
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
RuntimeError: If all providers fail
|
|
85
|
+
ValueError: If required fields are missing
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
>>> missive = send_missive(
|
|
89
|
+
... "EMAIL",
|
|
90
|
+
... body="Hello world",
|
|
91
|
+
... subject="Test",
|
|
92
|
+
... recipient_email="user@example.com",
|
|
93
|
+
... )
|
|
94
|
+
>>> print(missive.status)
|
|
95
|
+
MissiveStatus.SENT
|
|
96
|
+
"""
|
|
97
|
+
missive_type = missive_type.upper()
|
|
98
|
+
|
|
99
|
+
# Validate required fields based on type
|
|
100
|
+
if missive_type == "EMAIL" and not recipient_email:
|
|
101
|
+
raise ValueError("recipient_email required for EMAIL missives")
|
|
102
|
+
if missive_type == "EMAIL" and not subject:
|
|
103
|
+
raise ValueError("subject required for EMAIL missives")
|
|
104
|
+
if missive_type in ("SMS", "VOICE_CALL") and not recipient_phone:
|
|
105
|
+
raise ValueError(f"recipient_phone required for {missive_type} missives")
|
|
106
|
+
if (
|
|
107
|
+
missive_type in ("POSTAL", "POSTAL_REGISTERED")
|
|
108
|
+
and not recipient
|
|
109
|
+
and not recipient_email
|
|
110
|
+
):
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"recipient or recipient_email required for {missive_type} missives"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Create missive object
|
|
116
|
+
missive = Missive(
|
|
117
|
+
missive_type=missive_type,
|
|
118
|
+
body=body,
|
|
119
|
+
subject=subject,
|
|
120
|
+
recipient_email=recipient_email,
|
|
121
|
+
recipient_phone=recipient_phone,
|
|
122
|
+
recipient=recipient,
|
|
123
|
+
provider_options=kwargs.get("provider_options", {}),
|
|
124
|
+
is_registered=kwargs.get("is_registered", missive_type == "POSTAL_REGISTERED"),
|
|
125
|
+
requires_signature=kwargs.get("requires_signature", False),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Create sender and send
|
|
129
|
+
sender = MissiveSender(
|
|
130
|
+
providers_config=providers_config,
|
|
131
|
+
default_config=config,
|
|
132
|
+
sandbox=sandbox,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
sender.send(missive, enable_fallback=enable_fallback)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
missive.status = MissiveStatus.FAILED
|
|
139
|
+
missive.error_message = str(e)
|
|
140
|
+
raise
|
|
141
|
+
|
|
142
|
+
return missive
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Structured address representation with optional backend normalization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Dict, Mapping, Optional, Sequence, Tuple
|
|
7
|
+
|
|
8
|
+
_ADDRESS_KEYS = (
|
|
9
|
+
"address_line1",
|
|
10
|
+
"address_line2",
|
|
11
|
+
"address_line3",
|
|
12
|
+
"recipient_address_line1",
|
|
13
|
+
"recipient_address_line2",
|
|
14
|
+
"recipient_address_line3",
|
|
15
|
+
"sender_address_line1",
|
|
16
|
+
"sender_address_line2",
|
|
17
|
+
"sender_address_line3",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(slots=True)
|
|
22
|
+
class Address:
|
|
23
|
+
"""Lightweight container for structured postal addresses."""
|
|
24
|
+
|
|
25
|
+
line1: str = ""
|
|
26
|
+
line2: str = ""
|
|
27
|
+
line3: str = ""
|
|
28
|
+
postal_code: str = ""
|
|
29
|
+
city: str = ""
|
|
30
|
+
state: str = ""
|
|
31
|
+
country: str = ""
|
|
32
|
+
latitude: Optional[float] = None
|
|
33
|
+
longitude: Optional[float] = None
|
|
34
|
+
formatted: str = ""
|
|
35
|
+
backend_used: Optional[str] = None
|
|
36
|
+
backend_reference: Optional[str] = None
|
|
37
|
+
confidence: Optional[float] = None
|
|
38
|
+
suggestions: Sequence[Mapping[str, Any]] = field(default_factory=tuple)
|
|
39
|
+
warnings: Sequence[str] = field(default_factory=tuple)
|
|
40
|
+
errors: Sequence[str] = field(default_factory=tuple)
|
|
41
|
+
extras: Dict[str, Any] = field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
def is_empty(self) -> bool:
|
|
44
|
+
"""Return True when no user-level field is populated."""
|
|
45
|
+
return not any(
|
|
46
|
+
[
|
|
47
|
+
self.line1,
|
|
48
|
+
self.line2,
|
|
49
|
+
self.line3,
|
|
50
|
+
self.postal_code,
|
|
51
|
+
self.city,
|
|
52
|
+
self.state,
|
|
53
|
+
self.country,
|
|
54
|
+
]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def to_dict(self, *, include_empty: bool = False) -> Dict[str, Any]:
|
|
58
|
+
"""Serialize the address to a JSON-friendly dictionary."""
|
|
59
|
+
data = {
|
|
60
|
+
"line1": self.line1,
|
|
61
|
+
"line2": self.line2,
|
|
62
|
+
"line3": self.line3,
|
|
63
|
+
"postal_code": self.postal_code,
|
|
64
|
+
"city": self.city,
|
|
65
|
+
"state": self.state,
|
|
66
|
+
"country": self.country,
|
|
67
|
+
"latitude": self.latitude,
|
|
68
|
+
"longitude": self.longitude,
|
|
69
|
+
"formatted": self.formatted,
|
|
70
|
+
"backend_used": self.backend_used,
|
|
71
|
+
"backend_reference": self.backend_reference,
|
|
72
|
+
"confidence": self.confidence,
|
|
73
|
+
"suggestions": list(self.suggestions),
|
|
74
|
+
"warnings": list(self.warnings),
|
|
75
|
+
"errors": list(self.errors),
|
|
76
|
+
"extras": dict(self.extras),
|
|
77
|
+
}
|
|
78
|
+
if not include_empty:
|
|
79
|
+
data = {
|
|
80
|
+
key: value
|
|
81
|
+
for key, value in data.items()
|
|
82
|
+
if value not in ("", None, [], {})
|
|
83
|
+
}
|
|
84
|
+
return data
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def from_dict(cls, payload: Mapping[str, Any] | None) -> "Address":
|
|
88
|
+
"""Build an Address instance from a dictionary payload."""
|
|
89
|
+
if not payload:
|
|
90
|
+
return cls()
|
|
91
|
+
|
|
92
|
+
def _extract_line(key: str) -> str:
|
|
93
|
+
for alias in (
|
|
94
|
+
key,
|
|
95
|
+
key.replace("line", "_line"),
|
|
96
|
+
f"recipient_{key}",
|
|
97
|
+
f"sender_{key}",
|
|
98
|
+
):
|
|
99
|
+
if alias in payload and payload[alias]:
|
|
100
|
+
return str(payload[alias])
|
|
101
|
+
return ""
|
|
102
|
+
|
|
103
|
+
return cls(
|
|
104
|
+
line1=_extract_line("line1"),
|
|
105
|
+
line2=_extract_line("line2"),
|
|
106
|
+
line3=_extract_line("line3"),
|
|
107
|
+
postal_code=str(payload.get("postal_code") or payload.get("zip", "")),
|
|
108
|
+
city=str(payload.get("city") or payload.get("town") or ""),
|
|
109
|
+
state=str(payload.get("state") or payload.get("region") or ""),
|
|
110
|
+
country=str(payload.get("country") or payload.get("country_code") or ""),
|
|
111
|
+
latitude=_safe_float(payload.get("latitude")),
|
|
112
|
+
longitude=_safe_float(payload.get("longitude")),
|
|
113
|
+
formatted=str(
|
|
114
|
+
payload.get("formatted_address") or payload.get("formatted") or ""
|
|
115
|
+
),
|
|
116
|
+
backend_used=payload.get("backend_used") or payload.get("backend"),
|
|
117
|
+
backend_reference=payload.get("backend_reference")
|
|
118
|
+
or payload.get("reference_id")
|
|
119
|
+
or payload.get("address_reference"),
|
|
120
|
+
confidence=_safe_float(payload.get("confidence")),
|
|
121
|
+
suggestions=tuple(payload.get("suggestions") or ()),
|
|
122
|
+
warnings=tuple(payload.get("warnings") or ()),
|
|
123
|
+
errors=tuple(payload.get("errors") or ()),
|
|
124
|
+
extras=_extract_extras(payload),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def merge(self, other: "Address", *, prefer_other: bool = True) -> "Address":
|
|
128
|
+
"""Merge two addresses, optionally preferring `other` values when provided."""
|
|
129
|
+
|
|
130
|
+
def _select(current: Any, new_value: Any) -> Any:
|
|
131
|
+
if prefer_other and new_value not in ("", None):
|
|
132
|
+
return new_value
|
|
133
|
+
if not prefer_other and current not in ("", None):
|
|
134
|
+
return current
|
|
135
|
+
return new_value if new_value not in ("", None) else current
|
|
136
|
+
|
|
137
|
+
merged = Address(
|
|
138
|
+
line1=_select(self.line1, other.line1),
|
|
139
|
+
line2=_select(self.line2, other.line2),
|
|
140
|
+
line3=_select(self.line3, other.line3),
|
|
141
|
+
postal_code=_select(self.postal_code, other.postal_code),
|
|
142
|
+
city=_select(self.city, other.city),
|
|
143
|
+
state=_select(self.state, other.state),
|
|
144
|
+
country=_select(self.country, other.country),
|
|
145
|
+
latitude=_select(self.latitude, other.latitude),
|
|
146
|
+
longitude=_select(self.longitude, other.longitude),
|
|
147
|
+
formatted=_select(self.formatted, other.formatted),
|
|
148
|
+
backend_used=other.backend_used or self.backend_used,
|
|
149
|
+
backend_reference=other.backend_reference or self.backend_reference,
|
|
150
|
+
confidence=_select(self.confidence, other.confidence),
|
|
151
|
+
suggestions=other.suggestions or self.suggestions,
|
|
152
|
+
warnings=other.warnings or self.warnings,
|
|
153
|
+
errors=other.errors or self.errors,
|
|
154
|
+
extras={**self.extras, **other.extras},
|
|
155
|
+
)
|
|
156
|
+
return merged
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def normalize_with_backends(
|
|
160
|
+
cls,
|
|
161
|
+
backends_config: Sequence[Dict[str, Any]] | None,
|
|
162
|
+
*,
|
|
163
|
+
_operation: str = "validate",
|
|
164
|
+
min_confidence: Optional[float] = None,
|
|
165
|
+
**address_kwargs: Any,
|
|
166
|
+
) -> Tuple["Address", Dict[str, Any]]:
|
|
167
|
+
"""Call configured backends and return a normalized Address plus raw payload."""
|
|
168
|
+
if not backends_config:
|
|
169
|
+
return cls.from_dict(address_kwargs), {
|
|
170
|
+
"error": "No address backends configured"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
from .helpers import search_addresses
|
|
174
|
+
|
|
175
|
+
# Build query string from address components
|
|
176
|
+
address_parts = [
|
|
177
|
+
address_kwargs.get("address_line1"),
|
|
178
|
+
address_kwargs.get("address_line2"),
|
|
179
|
+
address_kwargs.get("address_line3"),
|
|
180
|
+
address_kwargs.get("postal_code"),
|
|
181
|
+
address_kwargs.get("city"),
|
|
182
|
+
address_kwargs.get("state"),
|
|
183
|
+
]
|
|
184
|
+
query = ", ".join(filter(None, address_parts)) or ""
|
|
185
|
+
|
|
186
|
+
# If query is empty but we have components, try a simpler query
|
|
187
|
+
if not query:
|
|
188
|
+
query = address_kwargs.get("address_line1") or ""
|
|
189
|
+
|
|
190
|
+
search_result = search_addresses(
|
|
191
|
+
backends_config=backends_config,
|
|
192
|
+
query=query,
|
|
193
|
+
country=address_kwargs.get("country"),
|
|
194
|
+
min_confidence=min_confidence,
|
|
195
|
+
limit=1,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Get first result or use error payload
|
|
199
|
+
results = search_result.get("results", [])
|
|
200
|
+
if results:
|
|
201
|
+
payload = results[0]
|
|
202
|
+
normalized_block = payload.get("normalized_address") or payload
|
|
203
|
+
else:
|
|
204
|
+
payload = search_result
|
|
205
|
+
normalized_block = {}
|
|
206
|
+
normalized = cls.from_dict(
|
|
207
|
+
{
|
|
208
|
+
**address_kwargs,
|
|
209
|
+
**_flatten_address_dict(normalized_block),
|
|
210
|
+
"backend_used": payload.get("backend_used"),
|
|
211
|
+
"confidence": payload.get("confidence"),
|
|
212
|
+
"warnings": payload.get("warnings"),
|
|
213
|
+
"errors": payload.get("errors"),
|
|
214
|
+
"suggestions": payload.get("suggestions"),
|
|
215
|
+
"backend_reference": payload.get("backend_reference")
|
|
216
|
+
or payload.get("reference_id")
|
|
217
|
+
or payload.get("address_reference"),
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
return normalized, payload
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _extract_extras(payload: Mapping[str, Any]) -> Dict[str, Any]:
|
|
224
|
+
extras = dict(payload.get("extras") or {})
|
|
225
|
+
for key in ("latitude", "longitude", "formatted_address"):
|
|
226
|
+
if key in payload and key not in extras:
|
|
227
|
+
extras[key] = payload[key]
|
|
228
|
+
for key in _ADDRESS_KEYS:
|
|
229
|
+
if key in payload and payload[key]:
|
|
230
|
+
extras.setdefault(key, payload[key])
|
|
231
|
+
return extras
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _flatten_address_dict(payload: Mapping[str, Any]) -> Dict[str, Any]:
|
|
235
|
+
flat: Dict[str, Any] = {}
|
|
236
|
+
for key in ("line1", "line2", "line3", "postal_code", "city", "state", "country"):
|
|
237
|
+
if key in payload and payload[key]:
|
|
238
|
+
flat[key] = payload[key]
|
|
239
|
+
suffixes = (
|
|
240
|
+
"line1",
|
|
241
|
+
"line2",
|
|
242
|
+
"line3",
|
|
243
|
+
"postal_code",
|
|
244
|
+
"city",
|
|
245
|
+
"state",
|
|
246
|
+
"country",
|
|
247
|
+
)
|
|
248
|
+
for key in _ADDRESS_KEYS:
|
|
249
|
+
value = payload.get(key)
|
|
250
|
+
if value and key.endswith(suffixes):
|
|
251
|
+
alias = key.split("_")[-1]
|
|
252
|
+
flat.setdefault(alias, value)
|
|
253
|
+
if "formatted_address" in payload and payload["formatted_address"]:
|
|
254
|
+
flat["formatted"] = payload["formatted_address"]
|
|
255
|
+
if "latitude" in payload:
|
|
256
|
+
flat["latitude"] = payload.get("latitude")
|
|
257
|
+
if "longitude" in payload:
|
|
258
|
+
flat["longitude"] = payload.get("longitude")
|
|
259
|
+
if "backend_reference" in payload and payload["backend_reference"]:
|
|
260
|
+
flat["backend_reference"] = payload["backend_reference"]
|
|
261
|
+
elif "address_reference" in payload and payload["address_reference"]:
|
|
262
|
+
flat["backend_reference"] = payload["address_reference"]
|
|
263
|
+
return flat
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _safe_float(value: Any) -> Optional[float]:
|
|
267
|
+
if value in (None, ""):
|
|
268
|
+
return None
|
|
269
|
+
try:
|
|
270
|
+
return float(value)
|
|
271
|
+
except (TypeError, ValueError):
|
|
272
|
+
return None
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Address verification backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .base import BaseAddressBackend
|
|
6
|
+
from .geocode_earth import GeocodeEarthAddressBackend
|
|
7
|
+
from .geoapify import GeoapifyAddressBackend
|
|
8
|
+
from .google_maps import GoogleMapsAddressBackend
|
|
9
|
+
from .here import HereAddressBackend
|
|
10
|
+
from .locationiq import LocationIQAddressBackend
|
|
11
|
+
from .maps_co import MapsCoAddressBackend
|
|
12
|
+
from .mapbox import MapboxAddressBackend
|
|
13
|
+
from .nominatim import NominatimAddressBackend
|
|
14
|
+
from .opencage import OpenCageAddressBackend
|
|
15
|
+
from .photon import PhotonAddressBackend
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"BaseAddressBackend",
|
|
19
|
+
"GeocodeEarthAddressBackend",
|
|
20
|
+
"GeoapifyAddressBackend",
|
|
21
|
+
"GoogleMapsAddressBackend",
|
|
22
|
+
"HereAddressBackend",
|
|
23
|
+
"LocationIQAddressBackend",
|
|
24
|
+
"MapsCoAddressBackend",
|
|
25
|
+
"MapboxAddressBackend",
|
|
26
|
+
"NominatimAddressBackend",
|
|
27
|
+
"OpenCageAddressBackend",
|
|
28
|
+
"PhotonAddressBackend",
|
|
29
|
+
]
|