allowances-api 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.
- allowances_api/__init__.py +26 -0
- allowances_api/_utils.py +24 -0
- allowances_api/client.py +100 -0
- allowances_api/exceptions.py +60 -0
- allowances_api/py.typed +1 -0
- allowances_api/resources.py +153 -0
- allowances_api-0.1.0.dist-info/METADATA +223 -0
- allowances_api-0.1.0.dist-info/RECORD +10 -0
- allowances_api-0.1.0.dist-info/WHEEL +4 -0
- allowances_api-0.1.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Python SDK for the Allowances API."""
|
|
2
|
+
|
|
3
|
+
from .client import AllowancesClient
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
AllowancesAPIError,
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
ForbiddenError,
|
|
8
|
+
NotFoundError,
|
|
9
|
+
RateLimitError,
|
|
10
|
+
TransportError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AllowancesClient",
|
|
16
|
+
"AllowancesAPIError",
|
|
17
|
+
"AuthenticationError",
|
|
18
|
+
"ForbiddenError",
|
|
19
|
+
"NotFoundError",
|
|
20
|
+
"RateLimitError",
|
|
21
|
+
"TransportError",
|
|
22
|
+
"ValidationError",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
|
allowances_api/_utils.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from datetime import date, datetime
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any, Dict, Mapping, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def serialize(value: Any) -> Any:
|
|
7
|
+
if isinstance(value, (date, datetime)):
|
|
8
|
+
return value.isoformat()
|
|
9
|
+
if isinstance(value, Enum):
|
|
10
|
+
return value.value
|
|
11
|
+
if isinstance(value, (list, tuple, set)):
|
|
12
|
+
return [serialize(item) for item in value]
|
|
13
|
+
return value
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def query_params(**values: Any) -> Dict[str, Any]:
|
|
17
|
+
return {key: serialize(value) for key, value in values.items() if value is not None}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def merge_params(params: Optional[Mapping[str, Any]], **values: Any) -> Dict[str, Any]:
|
|
21
|
+
merged = dict(params or {})
|
|
22
|
+
merged.update(query_params(**values))
|
|
23
|
+
return merged
|
|
24
|
+
|
allowances_api/client.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""HTTP client and authentication for the Allowances API."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Mapping, Optional
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from .exceptions import (
|
|
9
|
+
AllowancesAPIError,
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
ForbiddenError,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
TransportError,
|
|
15
|
+
ValidationError,
|
|
16
|
+
error_details,
|
|
17
|
+
)
|
|
18
|
+
from .resources import DSSRResource, DTMOResource, GSAResource
|
|
19
|
+
|
|
20
|
+
DEFAULT_BASE_URL = "https://api.allowancesapi.com/v1"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AllowancesClient:
|
|
24
|
+
"""Authenticated synchronous client for all public Allowances API endpoints."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
api_key: Optional[str] = None,
|
|
29
|
+
*,
|
|
30
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
31
|
+
timeout: float = 30.0,
|
|
32
|
+
http_client: Optional[httpx.Client] = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
key = api_key or os.getenv("ALLOWANCES_API_KEY")
|
|
35
|
+
if not key:
|
|
36
|
+
raise ValueError("Pass api_key or set the ALLOWANCES_API_KEY environment variable")
|
|
37
|
+
|
|
38
|
+
self._owns_client = http_client is None
|
|
39
|
+
self._http = http_client or httpx.Client(
|
|
40
|
+
base_url=base_url.rstrip("/"),
|
|
41
|
+
timeout=timeout,
|
|
42
|
+
headers={"X-API-Key": key, "User-Agent": "allowances-api-python/0.1.0"},
|
|
43
|
+
)
|
|
44
|
+
if http_client is not None:
|
|
45
|
+
self._http.headers["X-API-Key"] = key
|
|
46
|
+
self._http.headers.setdefault("User-Agent", "allowances-api-python/0.1.0")
|
|
47
|
+
|
|
48
|
+
self.dssr = DSSRResource(self)
|
|
49
|
+
self.dtmo = DTMOResource(self)
|
|
50
|
+
self.gsa = GSAResource(self)
|
|
51
|
+
|
|
52
|
+
def request(
|
|
53
|
+
self,
|
|
54
|
+
method: str,
|
|
55
|
+
path: str,
|
|
56
|
+
*,
|
|
57
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
58
|
+
) -> Any:
|
|
59
|
+
try:
|
|
60
|
+
response = self._http.request(method, path, params=params)
|
|
61
|
+
except httpx.HTTPError as exc:
|
|
62
|
+
raise TransportError(str(exc)) from exc
|
|
63
|
+
|
|
64
|
+
if response.is_success:
|
|
65
|
+
if response.status_code == 204 or not response.content:
|
|
66
|
+
return None
|
|
67
|
+
return response.json()
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
payload = response.json()
|
|
71
|
+
except ValueError:
|
|
72
|
+
payload = None
|
|
73
|
+
message, request_id = error_details(payload)
|
|
74
|
+
error_type = {
|
|
75
|
+
401: AuthenticationError,
|
|
76
|
+
403: ForbiddenError,
|
|
77
|
+
404: NotFoundError,
|
|
78
|
+
422: ValidationError,
|
|
79
|
+
429: RateLimitError,
|
|
80
|
+
}.get(response.status_code, AllowancesAPIError)
|
|
81
|
+
raise error_type(
|
|
82
|
+
message,
|
|
83
|
+
status_code=response.status_code,
|
|
84
|
+
request_id=request_id,
|
|
85
|
+
response=response,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def get(self, path: str, *, params: Optional[Mapping[str, Any]] = None) -> Any:
|
|
89
|
+
return self.request("GET", path, params=params)
|
|
90
|
+
|
|
91
|
+
def close(self) -> None:
|
|
92
|
+
if self._owns_client:
|
|
93
|
+
self._http.close()
|
|
94
|
+
|
|
95
|
+
def __enter__(self) -> "AllowancesClient":
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def __exit__(self, *args: Any) -> None:
|
|
99
|
+
self.close()
|
|
100
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Exceptions raised by the Allowances API client."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Mapping, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AllowancesAPIError(Exception):
|
|
7
|
+
"""An error response returned by the Allowances API."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
message: str,
|
|
12
|
+
*,
|
|
13
|
+
status_code: Optional[int] = None,
|
|
14
|
+
request_id: Optional[str] = None,
|
|
15
|
+
response: Any = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.message = message
|
|
19
|
+
self.status_code = status_code
|
|
20
|
+
self.request_id = request_id
|
|
21
|
+
self.response = response
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthenticationError(AllowancesAPIError):
|
|
25
|
+
"""The API key is missing or invalid."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ForbiddenError(AllowancesAPIError):
|
|
29
|
+
"""The current plan cannot access the requested resource."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class NotFoundError(AllowancesAPIError):
|
|
33
|
+
"""The requested data was not found."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ValidationError(AllowancesAPIError):
|
|
37
|
+
"""One or more request parameters are invalid."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RateLimitError(AllowancesAPIError):
|
|
41
|
+
"""The account's request limit was exceeded."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TransportError(AllowancesAPIError):
|
|
45
|
+
"""The request could not reach the API."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def error_details(payload: Any) -> tuple[str, Optional[str]]:
|
|
49
|
+
"""Extract the API's message and request ID from supported error envelopes."""
|
|
50
|
+
if not isinstance(payload, Mapping):
|
|
51
|
+
return "Allowances API request failed", None
|
|
52
|
+
|
|
53
|
+
detail = payload.get("detail", payload)
|
|
54
|
+
if isinstance(detail, Mapping):
|
|
55
|
+
return str(detail.get("message") or detail.get("detail") or detail), detail.get("request_id")
|
|
56
|
+
if isinstance(detail, list):
|
|
57
|
+
messages = [str(item.get("msg", item)) if isinstance(item, Mapping) else str(item) for item in detail]
|
|
58
|
+
return "; ".join(messages), None
|
|
59
|
+
return str(detail), None
|
|
60
|
+
|
allowances_api/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Resource clients mirroring the public v1 API routes."""
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Iterable, Optional, Union
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
|
|
7
|
+
from ._utils import query_params
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .client import AllowancesClient
|
|
11
|
+
|
|
12
|
+
DateLike = Union[str, date]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def segment(value: Any) -> str:
|
|
16
|
+
return quote(str(value), safe="")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Resource:
|
|
20
|
+
def __init__(self, client: "AllowancesClient") -> None:
|
|
21
|
+
self._client = client
|
|
22
|
+
|
|
23
|
+
def _get(self, path: str, **params: Any) -> Any:
|
|
24
|
+
# Friendly Python names for API parameters whose wire names are terse or singular.
|
|
25
|
+
if "types" in params:
|
|
26
|
+
params["type"] = params.pop("types")
|
|
27
|
+
if "query" in params:
|
|
28
|
+
params["q"] = params.pop("query")
|
|
29
|
+
return self._client.get(path, params=query_params(**params))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DSSRResource(Resource):
|
|
33
|
+
def allowances(
|
|
34
|
+
self, country: str, *, date: Optional[DateLike] = None,
|
|
35
|
+
types: Optional[Iterable[str]] = None, verification_url: bool = False,
|
|
36
|
+
) -> Any:
|
|
37
|
+
return self._get(f"/dssr/allowances/{segment(country)}", date=date, type=types,
|
|
38
|
+
verification_url=verification_url)
|
|
39
|
+
|
|
40
|
+
def allowances_by_location(self, country: str, location: str, **params: Any) -> Any:
|
|
41
|
+
return self._get(f"/dssr/allowances/{segment(country)}/location/{segment(location)}", **params)
|
|
42
|
+
|
|
43
|
+
def allowances_by_postcode(self, country: str, postcode: Union[str, int], **params: Any) -> Any:
|
|
44
|
+
return self._get(f"/dssr/allowances/{segment(country)}/postcode/{segment(postcode)}", **params)
|
|
45
|
+
|
|
46
|
+
def perdiem(self, country: str, **params: Any) -> Any:
|
|
47
|
+
return self._get(f"/dssr/perdiem/{segment(country)}", **params)
|
|
48
|
+
|
|
49
|
+
def perdiem_by_location(self, country: str, location: str, **params: Any) -> Any:
|
|
50
|
+
return self._get(f"/dssr/perdiem/{segment(country)}/location/{segment(location)}", **params)
|
|
51
|
+
|
|
52
|
+
def perdiem_by_postcode(self, country: str, postcode: Union[str, int], **params: Any) -> Any:
|
|
53
|
+
return self._get(f"/dssr/perdiem/{segment(country)}/postcode/{segment(postcode)}", **params)
|
|
54
|
+
|
|
55
|
+
def search_perdiem(self, query: str, *, date: Optional[DateLike] = None) -> Any:
|
|
56
|
+
return self._get("/dssr/perdiem/search", q=query, date=date)
|
|
57
|
+
|
|
58
|
+
def locations(self, *, query: Optional[str] = None, offset: int = 0, limit: int = 10) -> Any:
|
|
59
|
+
return self._get("/dssr/locations", q=query, offset=offset, limit=limit)
|
|
60
|
+
|
|
61
|
+
def countries(self, *, query: Optional[str] = None, offset: int = 0, limit: int = 20) -> Any:
|
|
62
|
+
return self._get("/dssr/countries", q=query, offset=offset, limit=limit)
|
|
63
|
+
|
|
64
|
+
def country(self, country: str) -> Any:
|
|
65
|
+
return self._get(f"/dssr/country/{segment(country)}")
|
|
66
|
+
|
|
67
|
+
def country_locations(self, country: str) -> Any:
|
|
68
|
+
return self._get(f"/dssr/country/{segment(country)}/locations")
|
|
69
|
+
|
|
70
|
+
def calculate_cola(
|
|
71
|
+
self, postcode: Union[str, int], *, family_size: int, base_salary: int,
|
|
72
|
+
date: Optional[DateLike] = None,
|
|
73
|
+
) -> Any:
|
|
74
|
+
return self._get(f"/dssr/cola-calculator/postcode/{segment(postcode)}",
|
|
75
|
+
family_size=family_size, base_salary=base_salary, date=date)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class DTMOResource(Resource):
|
|
79
|
+
def perdiem_conus(
|
|
80
|
+
self, state: str, *, date: Optional[DateLike] = None, year: Optional[int] = None,
|
|
81
|
+
query: Optional[str] = None, verification_url: bool = False,
|
|
82
|
+
offset: int = 0, limit: int = 50,
|
|
83
|
+
) -> Any:
|
|
84
|
+
return self._get(f"/dtmo/perdiem/conus/{segment(state)}", date=date, year=year, q=query,
|
|
85
|
+
verification_url=verification_url, offset=offset, limit=limit)
|
|
86
|
+
|
|
87
|
+
def perdiem_oconus(self, country: str, **params: Any) -> Any:
|
|
88
|
+
return self._get(f"/dtmo/perdiem/oconus/{segment(country)}", **params)
|
|
89
|
+
|
|
90
|
+
def search_perdiem(
|
|
91
|
+
self, query: str, *, date: Optional[DateLike] = None, year: Optional[int] = None,
|
|
92
|
+
verification_url: bool = False,
|
|
93
|
+
) -> Any:
|
|
94
|
+
return self._get("/dtmo/perdiem/search", q=query, date=date, year=year,
|
|
95
|
+
verification_url=verification_url)
|
|
96
|
+
|
|
97
|
+
def locations_conus(self, state: str) -> Any:
|
|
98
|
+
return self._get(f"/dtmo/locations/conus/{segment(state)}")
|
|
99
|
+
|
|
100
|
+
def locations_oconus(self, country: str, *, location: Optional[str] = None) -> Any:
|
|
101
|
+
return self._get(f"/dtmo/locations/oconus/{segment(country)}", location=location)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class GSAResource(Resource):
|
|
105
|
+
def perdiem_by_destination(self, destination_id: int, **params: Any) -> Any:
|
|
106
|
+
return self._get(f"/gsa/perdiem/destination/{destination_id}", **params)
|
|
107
|
+
|
|
108
|
+
def perdiem_by_state(self, state: str, **params: Any) -> Any:
|
|
109
|
+
return self._get(f"/gsa/perdiem/state/{segment(state)}", **params)
|
|
110
|
+
|
|
111
|
+
def perdiem_by_state_year(self, state: str, year: int, **params: Any) -> Any:
|
|
112
|
+
return self._get(f"/gsa/perdiem/state/{segment(state)}/year/{year}", **params)
|
|
113
|
+
|
|
114
|
+
def perdiem_by_zipcode(self, zipcode: Union[str, int], **params: Any) -> Any:
|
|
115
|
+
return self._get(f"/gsa/perdiem/zipcode/{segment(zipcode)}", **params)
|
|
116
|
+
|
|
117
|
+
def perdiem_by_zipcode_year(self, zipcode: Union[str, int], year: int, **params: Any) -> Any:
|
|
118
|
+
return self._get(f"/gsa/perdiem/zipcode/{segment(zipcode)}/year/{year}", **params)
|
|
119
|
+
|
|
120
|
+
def zipcodes(self, state: str, *, offset: int = 0, limit: int = 20) -> Any:
|
|
121
|
+
return self._get(f"/gsa/zipcodes/{segment(state)}", offset=offset, limit=limit)
|
|
122
|
+
|
|
123
|
+
def zipcode(self, zipcode: Union[str, int]) -> Any:
|
|
124
|
+
return self._get(f"/gsa/zipcode/{segment(zipcode)}")
|
|
125
|
+
|
|
126
|
+
def counties(
|
|
127
|
+
self, *, query: Optional[str] = None, state: Optional[str] = None,
|
|
128
|
+
offset: int = 0, limit: int = 20,
|
|
129
|
+
) -> Any:
|
|
130
|
+
return self._get("/gsa/counties", q=query, state=state, offset=offset, limit=limit)
|
|
131
|
+
|
|
132
|
+
def destination(self, destination_id: int) -> Any:
|
|
133
|
+
return self._get(f"/gsa/destination/{destination_id}")
|
|
134
|
+
|
|
135
|
+
def destinations(self, *, offset: int = 0, limit: int = 100) -> Any:
|
|
136
|
+
return self._get("/gsa/destinations", offset=offset, limit=limit)
|
|
137
|
+
|
|
138
|
+
def states(self) -> Any:
|
|
139
|
+
return self._get("/gsa/states")
|
|
140
|
+
|
|
141
|
+
def mileage(
|
|
142
|
+
self, *, date: Optional[DateLike] = None, mileage_type: Optional[str] = None,
|
|
143
|
+
verification_url: bool = False,
|
|
144
|
+
) -> Any:
|
|
145
|
+
return self._get("/gsa/mileage", date=date, type=mileage_type,
|
|
146
|
+
verification_url=verification_url)
|
|
147
|
+
|
|
148
|
+
def vehicle_rates(
|
|
149
|
+
self, rate_type: str, sin: str, *, year: Optional[int] = None,
|
|
150
|
+
verification_url: bool = False,
|
|
151
|
+
) -> Any:
|
|
152
|
+
return self._get(f"/gsa/vehicle-rates/{segment(rate_type)}/{segment(sin)}", year=year,
|
|
153
|
+
verification_url=verification_url)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: allowances-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Allowances API
|
|
5
|
+
Project-URL: Homepage, https://www.allowancesapi.com
|
|
6
|
+
Project-URL: Documentation, https://www.allowancesapi.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/allowances-api/allowances-api-python
|
|
8
|
+
Project-URL: Issues, https://github.com/allowances-api/allowances-api-python/issues
|
|
9
|
+
Author-email: Allowances API <contact@allowancesapi.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: allowances,dssr,dtmo,gsa,per-diem,travel
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: httpx<1,>=0.25
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.9; extra == 'dev'
|
|
30
|
+
Requires-Dist: twine>=6; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# Allowances API Python SDK
|
|
34
|
+
|
|
35
|
+
The official Python client for [Allowances API](https://www.allowancesapi.com), a normalized API for U.S. government travel and overseas compensation data.
|
|
36
|
+
|
|
37
|
+
The API brings together rates published by three different government sources:
|
|
38
|
+
|
|
39
|
+
- **DSSR** — overseas post allowance (COLA), hardship differential, danger pay, education allowance, living quarters allowance, and foreign per diem.
|
|
40
|
+
- **DTMO** — military per diem for CONUS and OCONUS locations.
|
|
41
|
+
- **GSA** — civilian CONUS per diem, mileage reimbursement, and Fleet vehicle leasing rates.
|
|
42
|
+
|
|
43
|
+
This SDK handles API-key authentication, URL construction, parameter serialization, response decoding, and useful Python exceptions. It is useful for travel platforms, expense systems, payroll and mobility tools, government contractors, and internal applications that need current or historical allowance data without maintaining scrapers for several unrelated sources.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install allowances-api
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Python 3.9 or newer is required.
|
|
52
|
+
|
|
53
|
+
## Authentication
|
|
54
|
+
|
|
55
|
+
Create an API key at [allowancesapi.com](https://www.allowancesapi.com), then provide it directly:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from allowances_api import AllowancesClient
|
|
59
|
+
|
|
60
|
+
client = AllowancesClient("ak_live_your_key")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For applications, keeping the key in an environment variable is preferable. The client will find your api key if you set it explicitely as ALLOWANCES_API_KEY.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
export ALLOWANCES_API_KEY="ak_live_your_key"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from allowances_api import AllowancesClient
|
|
71
|
+
|
|
72
|
+
client = AllowancesClient() # reads ALLOWANCES_API_KEY
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Every request sends the key in the API's `X-API-Key` header. Do not embed a secret API key in browser or mobile client code.
|
|
76
|
+
|
|
77
|
+
## Quick start
|
|
78
|
+
|
|
79
|
+
Use the client as a context manager so its connection pool is closed automatically:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from datetime import date
|
|
83
|
+
from allowances_api import AllowancesClient
|
|
84
|
+
|
|
85
|
+
with AllowancesClient() as client:
|
|
86
|
+
# Overseas allowances for Nairobi
|
|
87
|
+
kenya = client.dssr.allowances_by_location(
|
|
88
|
+
"KE",
|
|
89
|
+
"Nairobi",
|
|
90
|
+
date=date.today(),
|
|
91
|
+
type=["cola", "hardship", "danger_pay"],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Military OCONUS per diem
|
|
95
|
+
australia = client.dtmo.perdiem_oconus("AU", query="Adelaide")
|
|
96
|
+
|
|
97
|
+
# Civilian CONUS per diem
|
|
98
|
+
albany = client.gsa.perdiem_by_state("NY", location="Albany")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Responses are returned as decoded dictionaries or lists, matching the documented JSON response exactly.
|
|
102
|
+
|
|
103
|
+
## Common use cases
|
|
104
|
+
|
|
105
|
+
### Look up an overseas post allowance and calculate COLA
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
rates = client.dssr.allowances_by_postcode("BE", "10112", types=["cola"])
|
|
109
|
+
|
|
110
|
+
estimate = client.dssr.calculate_cola(
|
|
111
|
+
"10112",
|
|
112
|
+
family_size=4,
|
|
113
|
+
base_salary=100_000,
|
|
114
|
+
)
|
|
115
|
+
print(estimate["annual_cola"], estimate["biweekly_cola"])
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Retrieve per diem by source
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
# Department of State foreign per diem
|
|
122
|
+
dssr = client.dssr.perdiem_by_location("KE", "Nairobi")
|
|
123
|
+
|
|
124
|
+
# Defense Travel Management Office military rates
|
|
125
|
+
dtmo = client.dtmo.perdiem_conus("US-CO", query="Aspen")
|
|
126
|
+
|
|
127
|
+
# General Services Administration civilian rates
|
|
128
|
+
gsa = client.gsa.perdiem_by_zipcode("12207")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Mileage and GSA Fleet rates
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
mileage = client.gsa.mileage(mileage_type="automobile")
|
|
135
|
+
fleet = client.gsa.vehicle_rates("standard", "10B", year=2026)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Browse locations
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
countries = client.dssr.countries(query="United")
|
|
142
|
+
dssr_locations = client.dssr.country_locations("DE")
|
|
143
|
+
dtmo_locations = client.dtmo.locations_oconus("AU", location="Sydney")
|
|
144
|
+
states = client.gsa.states()
|
|
145
|
+
destinations = client.gsa.destinations(limit=50)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Errors
|
|
149
|
+
|
|
150
|
+
The SDK maps common status codes to specific exceptions:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from allowances_api import ForbiddenError, NotFoundError, RateLimitError
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
result = client.dssr.perdiem("XX")
|
|
157
|
+
except NotFoundError as exc:
|
|
158
|
+
print(exc.status_code, exc.message, exc.request_id)
|
|
159
|
+
except ForbiddenError:
|
|
160
|
+
print("This request is not included in the current plan")
|
|
161
|
+
except RateLimitError:
|
|
162
|
+
print("Request limit reached")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Available exception classes are `AuthenticationError`, `ForbiddenError`, `NotFoundError`, `ValidationError`, `RateLimitError`, `TransportError`, and the base `AllowancesAPIError`.
|
|
166
|
+
|
|
167
|
+
## Configuration
|
|
168
|
+
|
|
169
|
+
Use a different endpoint or timeout for local development and testing:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
client = AllowancesClient(
|
|
173
|
+
"test-key",
|
|
174
|
+
base_url="http://localhost:8000/v1",
|
|
175
|
+
timeout=10,
|
|
176
|
+
)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
An existing `httpx.Client` can be injected for custom transports, proxies, telemetry, or testing:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
import httpx
|
|
183
|
+
from allowances_api import AllowancesClient
|
|
184
|
+
|
|
185
|
+
transport = httpx.HTTPTransport(retries=2)
|
|
186
|
+
http = httpx.Client(base_url="https://api.allowancesapi.com/v1", transport=transport)
|
|
187
|
+
client = AllowancesClient("ak_live_your_key", http_client=http)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
When injecting a client, its lifecycle remains owned by the caller.
|
|
191
|
+
|
|
192
|
+
## Endpoint mapping
|
|
193
|
+
|
|
194
|
+
SDK methods follow the API's three source groups:
|
|
195
|
+
|
|
196
|
+
- `client.dssr`: allowances, per diem, COLA calculator, countries, and locations.
|
|
197
|
+
- `client.dtmo`: CONUS/OCONUS per diem, search, and locations.
|
|
198
|
+
- `client.gsa`: destination/state/ZIP per diem, ZIP and county metadata, mileage, and vehicle rates.
|
|
199
|
+
|
|
200
|
+
See the complete [API documentation](https://www.allowancesapi.com/docs) for response fields, plan limits, and data-source details.
|
|
201
|
+
|
|
202
|
+
## Development and publishing
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
python -m venv .venv
|
|
206
|
+
source .venv/bin/activate
|
|
207
|
+
pip install -e '.[dev]'
|
|
208
|
+
pytest
|
|
209
|
+
ruff check .
|
|
210
|
+
python -m build
|
|
211
|
+
twine check dist/*
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Publish a release after updating the version in `pyproject.toml` and `src/allowances_api/__init__.py`:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
twine upload dist/*
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT
|
|
223
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
allowances_api/__init__.py,sha256=_U5ICCLtZINjCh7fmxIlYYtmJPn3W9z6y1-8OpEufHA,482
|
|
2
|
+
allowances_api/_utils.py,sha256=nEspXOgbEhU8m_pqRqll58qCmRhtbuhpaIGJu1Snkh8,717
|
|
3
|
+
allowances_api/client.py,sha256=4Hp2kEL31BY3FiGBNUy_yIjqdzuP3tvyVo_GstwoWkc,2936
|
|
4
|
+
allowances_api/exceptions.py,sha256=w7PF5iillU6aAE7qBkjuWOqQJhOSlBOdPYgSpFoN4fg,1781
|
|
5
|
+
allowances_api/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
6
|
+
allowances_api/resources.py,sha256=tMPI4qLL-DvdZNEYK5EDAhtVbjpM-HZRtbBUzZEY6rw,6730
|
|
7
|
+
allowances_api-0.1.0.dist-info/METADATA,sha256=XGfTCoVhzKjPQMI6cTMbT0tIZkVK1b0kGe53AvXUjas,6943
|
|
8
|
+
allowances_api-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
9
|
+
allowances_api-0.1.0.dist-info/licenses/LICENSE,sha256=Yk7ewbhJM5FCcIL7KaoBH5Oi2uz5LRgGUTaVlhYSONU,1072
|
|
10
|
+
allowances_api-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Allowances API
|
|
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.
|
|
22
|
+
|