service-objects 0.1.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.
@@ -0,0 +1,7 @@
1
+ def __getattr__(name):
2
+ if name == "AV3":
3
+ from .av3 import AV3
4
+ return AV3
5
+ raise AttributeError(f"module {__name__} has no attribute {name}")
6
+
7
+ __all__ = ["AV3"]
service_objects/av3.py ADDED
@@ -0,0 +1,224 @@
1
+ """
2
+ Service Objects, Inc.
3
+
4
+ AV3 client for validating and standardizing US addresses
5
+ using Service Objects AV3 API. Handles production/trial/backup
6
+ endpoints, fallback logic, and JSON parsing.
7
+ """
8
+
9
+ from __future__ import annotations
10
+ from typing import Any
11
+ from .base import BaseClient
12
+ import requests
13
+
14
+ class AV3(BaseClient):
15
+ _ENDPOINTS = {
16
+ "production": "https://sws.serviceobjects.com/AV3/api.svc",
17
+ "backup": "https://swsbackup.serviceobjects.com/AV3/api.svc",
18
+ "trial": "https://trial.serviceobjects.com/AV3/api.svc",
19
+ }
20
+
21
+ def __init__(self, *, license_key: str, environment: str = "production", **kwargs):
22
+ if environment not in self._ENDPOINTS:
23
+ raise ValueError(f"Unknown environment: {environment}")
24
+
25
+ self.environment = environment
26
+ self.primary_url = self._ENDPOINTS[environment]
27
+ self.backup_url = (
28
+ self._ENDPOINTS["backup"]
29
+ if environment == "production"
30
+ else None
31
+ )
32
+
33
+ super().__init__(
34
+ base_url=self.primary_url,
35
+ license_key=license_key,
36
+ **kwargs
37
+ )
38
+
39
+ def _request_with_fallback(
40
+ self,
41
+ *,
42
+ path: str,
43
+ params: dict[str, Any],
44
+ ) -> dict:
45
+ try:
46
+ data = self._request(path=path, params=params)
47
+
48
+ if self.environment == "production" and data.get("Error"):
49
+ type_code = (data["Error"] or {}).get("TypeCode")
50
+ if type_code == "3" and self.backup_url:
51
+ data = self._request_direct(self.backup_url, path, params)
52
+
53
+ return data
54
+
55
+ except requests.RequestException as e:
56
+ if self.environment == "production" and self.backup_url:
57
+ data = self._request_direct(self.backup_url, path, params)
58
+ if data.get("Error"):
59
+ raise RuntimeError(f"AV3 backup error: {data['Error']}") from e
60
+ return data
61
+
62
+ raise RuntimeError(f"AV3 {self.environment} error: {str(e)}") from e
63
+
64
+ def _request_direct(self, base_url: str, path: str, params: dict[str, Any]) -> dict:
65
+ p = dict(params)
66
+ p["LicenseKey"] = self.license_key
67
+ url = f"{base_url.rstrip('/')}/{path.lstrip('/')}"
68
+ resp = self.session.get(url, params=p, timeout=self.timeout)
69
+ resp.raise_for_status()
70
+ return resp.json()
71
+
72
+ def GetBestMatches(
73
+ self,
74
+ *,
75
+ business_name: str = "",
76
+ address: str,
77
+ address_2: str = "",
78
+ city: str = "",
79
+ state: str = "",
80
+ postal_code: str = "",
81
+ **extra_params: Any,
82
+ ) -> dict[str, Any]:
83
+ """
84
+ Validate and standardize a US address using AV3.
85
+
86
+ Args:
87
+ business_name: Company name to assist suite parsing.
88
+ address: Primary street address (e.g., "123 Main St"). Required.
89
+ address_2: Secondary address information (e.g., "Apt 4B", "C/O John Smith").
90
+ city: City name. Required if postal_code is not provided.
91
+ state: State code or full name. Required if postal_code is not provided.
92
+ postal_code: 5- or 9-digit ZIP. Required if city/state are not provided.
93
+ **extra_params: Additional AV3 query parameters.
94
+
95
+ Returns:
96
+ Parsed JSON response containing address candidates, corrections, or an error payload.
97
+
98
+ Raises:
99
+ RuntimeError: If the service returns an error payload or all endpoints are unreachable.
100
+ """
101
+ params = {
102
+ "BusinessName": business_name,
103
+ "Address": address,
104
+ "Address2": address_2,
105
+ "City": city,
106
+ "State": state,
107
+ "PostalCode": postal_code,
108
+ **extra_params,
109
+ }
110
+
111
+ return self._request_with_fallback(
112
+ path="GetBestMatchesJson",
113
+ params=params,
114
+ )
115
+
116
+ def GetBestMatchesSingleLine(
117
+ self,
118
+ *,
119
+ business_name: str = "",
120
+ address: str,
121
+ **extra_params: Any,
122
+ ) -> dict[str, Any]:
123
+ """
124
+ Validate and standardize a single-line US address using AV3.
125
+
126
+ Args:
127
+ business_name: Company name to assist suite parsing.
128
+ address: Full address in one line (e.g., "123 Main St, Anytown CA 99999").
129
+ **extra_params: Additional AV3 query parameters.
130
+
131
+ Returns:
132
+ Parsed JSON response containing the best match candidate or an error payload.
133
+
134
+ Raises:
135
+ RuntimeError: If the service returns an error payload or all endpoints are unreachable.
136
+ """
137
+
138
+ params = {
139
+ "BusinessName": business_name,
140
+ "Address": address,
141
+ **extra_params,
142
+ }
143
+
144
+ return self._request_with_fallback(
145
+ path="GetBestMatchesSingleLineJson",
146
+ params=params,
147
+ )
148
+
149
+ def ValidateCityStateZip(
150
+ self,
151
+ *,
152
+ city: str = "",
153
+ state: str = "",
154
+ zip: str = "",
155
+ **extra_params: Any,
156
+ ) -> dict[str, Any]:
157
+ """
158
+ Validate a City/State/ZIP combination using AV3.
159
+
160
+ Args:
161
+ city: City name. Required if postal_code is not provided.
162
+ state: State code or full name. Required if postal_code is not provided.
163
+ zip: 5- or 9-digit ZIP. Required if city/state are not provided.
164
+ **extra_params: Additional AV3 query parameters.
165
+
166
+ Returns:
167
+ Parsed JSON response containing location validation results or an error payload.
168
+
169
+ Raises:
170
+ RuntimeError: If the service returns an error payload or all endpoints are unreachable.
171
+ """
172
+ params = {
173
+ "City": city,
174
+ "State": state,
175
+ "ZIP": zip,
176
+ **extra_params,
177
+ }
178
+
179
+ return self._request_with_fallback(
180
+ path="ValidateCityStateZipJson",
181
+ params=params,
182
+ )
183
+
184
+ def GetSecondaryNumbers(
185
+ self,
186
+ *,
187
+ address: str,
188
+ city: str = "",
189
+ state: str = "",
190
+ postal_code: str = "",
191
+ **extra_params: Any,
192
+ ) -> dict[str, Any]:
193
+ """
194
+ Retrieve potential secondary numbers (unit/suite) for a U.S. address using AV3.
195
+
196
+ This operation complements GetBestMatches by returning plausible secondary numbers
197
+ when the input address is missing or has incorrect unit information.
198
+
199
+ Args:
200
+ address: Primary street address (e.g., "123 Main St"). Required.
201
+ city: City name. Required if postal_code is not provided.
202
+ state: State code or full name. Required if postal_code is not provided.
203
+ postal_code: 5- or 9-digit ZIP. Required if city/state are not provided.
204
+ **extra_params: Additional AV3 query parameters.
205
+
206
+ Returns:
207
+ Parsed JSON response containing validated address elements and a list of
208
+ possible secondary numbers, or an error payload.
209
+
210
+ Raises:
211
+ RuntimeError: If the service returns an error payload or all endpoints are unreachable.
212
+ """
213
+ params = {
214
+ "Address": address,
215
+ "City": city,
216
+ "State": state,
217
+ "PostalCode": postal_code,
218
+ **extra_params,
219
+ }
220
+
221
+ return self._request_with_fallback(
222
+ path="GetSecondaryNumbersJson",
223
+ params=params,
224
+ )
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Optional
3
+ import requests
4
+
5
+ class BaseClient:
6
+ def __init__(self, *, base_url: str, license_key: str, session: Optional[requests.Session] = None, timeout: float = 10.0):
7
+ if not license_key:
8
+ raise ValueError("license_key is required")
9
+ self.base_url = base_url.rstrip("/")
10
+ self.license_key = license_key
11
+ self.session = session or requests.Session()
12
+ self.timeout = timeout
13
+
14
+ def _request(self, *, path: str, params: dict[str, Any] | None = None) -> Any:
15
+ params = dict(params or {})
16
+ params["LicenseKey"] = self.license_key
17
+
18
+ url = f"{self.base_url}/{path.lstrip('/')}"
19
+ resp = self.session.get(url, params=params, timeout=self.timeout)
20
+ resp.raise_for_status()
21
+ return resp.json()
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: service-objects
3
+ Version: 0.1.0
4
+ Summary: Python client for Service Objects APIs
5
+ Project-URL: Homepage, https://github.com/serviceobjects/service-objects
6
+ Author-email: Jan Rehorik <jrehorik@serviceobjects.com>
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.9
10
+ Requires-Dist: httpx>=0.27
11
+ Requires-Dist: pydantic>=2.0
12
+ Requires-Dist: requests>=2.31
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest; extra == 'dev'
15
+ Requires-Dist: requests-mock; extra == 'dev'
@@ -0,0 +1,7 @@
1
+ service_objects/__init__.py,sha256=lvEp3S2sh4PIMkaOO6OIrmfF8Bz2wTQnpJu00zf5ttc,188
2
+ service_objects/av3.py,sha256=de1W8JzrATa-ovC6Dt29fyZFktpVHXo3JPVwKQm_6RQ,7654
3
+ service_objects/base.py,sha256=GHGUWYqZrDrh-sbrwR5LUgxQ5TJya05-v7P29icCWqk,855
4
+ service_objects-0.1.0.dist-info/METADATA,sha256=wDGCyZIqmfIEncme37Zj206jxjTfkl4aoLgpcQO5_GY,483
5
+ service_objects-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
+ service_objects-0.1.0.dist-info/licenses/LICENSE,sha256=ul3mBFUa57euquRnv1vpwLouANels2Wz-A8YYfh9CEw,1097
7
+ service_objects-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Service Objects, Inc.
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.