macadress 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.
- macadress-1.0.0/.gitignore +14 -0
- macadress-1.0.0/CHANGELOG.md +20 -0
- macadress-1.0.0/LICENSE +21 -0
- macadress-1.0.0/PKG-INFO +226 -0
- macadress-1.0.0/README.md +195 -0
- macadress-1.0.0/examples/01_vendor_name.py +12 -0
- macadress-1.0.0/examples/02_full_lookup.py +30 -0
- macadress-1.0.0/examples/03_batch.py +19 -0
- macadress-1.0.0/examples/04_search_vendors.py +19 -0
- macadress-1.0.0/examples/05_async.py +27 -0
- macadress-1.0.0/pyproject.toml +61 -0
- macadress-1.0.0/src/macadress/__init__.py +66 -0
- macadress-1.0.0/src/macadress/_client.py +262 -0
- macadress-1.0.0/src/macadress/_transport.py +117 -0
- macadress-1.0.0/src/macadress/_version.py +7 -0
- macadress-1.0.0/src/macadress/enums.py +93 -0
- macadress-1.0.0/src/macadress/errors.py +88 -0
- macadress-1.0.0/src/macadress/models.py +269 -0
- macadress-1.0.0/src/macadress/py.typed +0 -0
- macadress-1.0.0/tests/conftest.py +72 -0
- macadress-1.0.0/tests/test_async_client.py +56 -0
- macadress-1.0.0/tests/test_client.py +160 -0
- macadress-1.0.0/tests/test_errors.py +85 -0
- macadress-1.0.0/tests/test_models.py +81 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this package are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [1.0.0] - 2026-09-02
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `Client` and `AsyncClient` with `vendor()`, `lookup()`, `batch()`, `search_vendors()`, `health()` and a raw `request()` escape hatch.
|
|
14
|
+
- Typed dataclass results: `MacResult`, `BatchItem`, `Device`, `Meta`, `VendorBlock`, `VendorSearchResult`, each with a `raw` dict and (on `MacResult`) a `get("dotted.path")` accessor.
|
|
15
|
+
- `str` enums (`BlockType`, `TransmissionType`, `AdministrationType`, `RandomizationConfidence`, `DeviceCategory`); unknown values pass through as plain strings.
|
|
16
|
+
- Exception hierarchy under `MacadressError`: `APIError`, `InvalidMACError`, `AuthenticationError`, `RateLimitError` (`retry_after`), `QuotaExceededError`, `TransportError`, `ConfigurationError`.
|
|
17
|
+
- One runtime dependency, `httpx`. `py.typed`; ships type information.
|
|
18
|
+
|
|
19
|
+
[Unreleased]: https://github.com/sapisos/macadress-python/compare/v1.0.0...HEAD
|
|
20
|
+
[1.0.0]: https://github.com/sapisos/macadress-python/releases/tag/v1.0.0
|
macadress-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ApisOS FZE
|
|
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.
|
macadress-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: macadress
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python client for the macadress.com MAC address and OUI vendor lookup API.
|
|
5
|
+
Project-URL: Homepage, https://macadress.com
|
|
6
|
+
Project-URL: Documentation, https://macadress.com/docs
|
|
7
|
+
Project-URL: Source, https://github.com/sapisos/macadress-python
|
|
8
|
+
Project-URL: Issues, https://github.com/sapisos/macadress-python/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/sapisos/macadress-python/blob/main/CHANGELOG.md
|
|
10
|
+
Author: ApisOS FZE
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: api-client,ieee,mac-address,mac-vendor,macadress,oui,sdk,vendor-lookup
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
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: Topic :: Internet
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# macadress
|
|
33
|
+
|
|
34
|
+
Official Python client for the [macadress.com](https://macadress.com) MAC address
|
|
35
|
+
and OUI vendor lookup API.
|
|
36
|
+
|
|
37
|
+
- Vendor name, OUI, IEEE block, country, address type, EUI-64 / IPv6 link-local, randomization confidence, device guess
|
|
38
|
+
- Keyless vendor-name lookup, plus keyed single / batch / directory-search calls
|
|
39
|
+
- Typed dataclass results with a `raw` escape hatch, typed exceptions per failure mode
|
|
40
|
+
- Sync `Client` and async `AsyncClient` from the same API, one dependency (`httpx`)
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from macadress import Client
|
|
44
|
+
|
|
45
|
+
mac = Client("mk_live_xxx")
|
|
46
|
+
|
|
47
|
+
mac.vendor("00:03:93:AB:12:34") # "Apple, Inc." (no API key required)
|
|
48
|
+
mac.lookup("00:03:93:AB:12:34").country # "US"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install macadress
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Requires Python 3.10+.
|
|
58
|
+
|
|
59
|
+
## Getting a key
|
|
60
|
+
|
|
61
|
+
`vendor()` needs no key. Everything else does. A free key (1,000 lookups a day) is
|
|
62
|
+
instant at [macadress.com/signup](https://macadress.com/signup); see
|
|
63
|
+
[pricing](https://macadress.com/pricing) for more.
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
### Create a client
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from macadress import Client
|
|
71
|
+
|
|
72
|
+
mac = Client("mk_live_xxx")
|
|
73
|
+
|
|
74
|
+
# keyless: only vendor() will work
|
|
75
|
+
anon = Client()
|
|
76
|
+
|
|
77
|
+
# options
|
|
78
|
+
mac = Client(
|
|
79
|
+
"mk_live_xxx",
|
|
80
|
+
base_url="https://api.macadress.com", # change only for a self-hosted deployment
|
|
81
|
+
timeout=10.0,
|
|
82
|
+
headers={"X-Trace": "my-app"},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# reuse / close
|
|
86
|
+
with Client("mk_live_xxx") as mac:
|
|
87
|
+
...
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### `vendor()` - name only, no key
|
|
91
|
+
|
|
92
|
+
Returns `None` when the address is valid but has no vendor to report
|
|
93
|
+
(unregistered, private, or locally administered / randomized).
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
mac.vendor("00:03:93:AB:12:34") # "Apple, Inc."
|
|
97
|
+
mac.vendor("02:1a:2b:3c:4d:5e") # None
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`:`, `-`, `.` and space grouping are all accepted, as is a bare 12-hex string.
|
|
101
|
+
|
|
102
|
+
### `lookup()` - full analysis
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
r = mac.lookup("3C:22:FB:12:34:56")
|
|
106
|
+
|
|
107
|
+
r.organization # str | None
|
|
108
|
+
r.vendor_lookup_reliable # bool (False for a private block / LAA)
|
|
109
|
+
r.oui # "3C:22:FB"
|
|
110
|
+
r.matched_prefix # full matched block at its real width
|
|
111
|
+
r.block_type # BlockType.MA_L (compares equal to "MA-L")
|
|
112
|
+
r.country # "US" | None
|
|
113
|
+
r.administration_type # AdministrationType.UNIVERSALLY_ADMINISTERED | ...
|
|
114
|
+
r.potentially_randomized # bool
|
|
115
|
+
r.randomization_confidence # RandomizationConfidence.NONE | POSSIBLE | LIKELY
|
|
116
|
+
r.eui64 # "3E:22:FB:FF:FE:12:34:56" | None
|
|
117
|
+
r.ipv6_link_local # "fe80::3e22:fbff:fe12:3456" | None
|
|
118
|
+
r.device.category # DeviceCategory.UNKNOWN (usually)
|
|
119
|
+
r.explanation # plain-English summary
|
|
120
|
+
r.meta.database_version # "2026-08-30"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Any field not covered by an attribute is still reachable:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
r.get("vendor_location.city") # dotted path into r.raw, default None
|
|
127
|
+
r.raw # the decoded payload as given
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### `batch()` - up to 100 at once
|
|
131
|
+
|
|
132
|
+
Results come back in input order; check each item.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
for item in mac.batch(["00:03:93:00:00:00", "3C:22:FB:00:00:00", "bad"]):
|
|
136
|
+
if item.failed:
|
|
137
|
+
print(item.input, "->", item.error)
|
|
138
|
+
else:
|
|
139
|
+
print(item.input, "->", item.organization)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Raises `ValueError` (no request made) if the iterable is empty or has more than
|
|
143
|
+
`macadress.MAX_BATCH_SIZE` (100) entries.
|
|
144
|
+
|
|
145
|
+
### `search_vendors()` - the directory
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
result = mac.search_vendors("Cisco", country="US", limit=20)
|
|
149
|
+
|
|
150
|
+
result.total # total matches, ignoring the limit
|
|
151
|
+
for block in result:
|
|
152
|
+
print(block.block_type, block.organization, block.country)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### `health()`
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
mac.health() # bool, keyless, uncounted; a transport failure is False
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Async
|
|
162
|
+
|
|
163
|
+
`AsyncClient` mirrors `Client` method for method:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
import asyncio
|
|
167
|
+
from macadress import AsyncClient
|
|
168
|
+
|
|
169
|
+
async def main():
|
|
170
|
+
async with AsyncClient("mk_live_xxx") as mac:
|
|
171
|
+
print(await mac.vendor("00:03:93:AB:12:34"))
|
|
172
|
+
r = await mac.lookup("3C:22:FB:12:34:56")
|
|
173
|
+
print(r.organization)
|
|
174
|
+
|
|
175
|
+
asyncio.run(main())
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Errors
|
|
179
|
+
|
|
180
|
+
Every failure is a `MacadressError`.
|
|
181
|
+
|
|
182
|
+
| Class | When |
|
|
183
|
+
|---|---|
|
|
184
|
+
| `InvalidMACError` | HTTP 400, the input did not parse |
|
|
185
|
+
| `AuthenticationError` | HTTP 401, missing or invalid API key |
|
|
186
|
+
| `RateLimitError` | HTTP 429, per-minute rate exceeded. `.retry_after` (seconds) when sent |
|
|
187
|
+
| `QuotaExceededError` | HTTP 429, billing-cycle quota spent. Subclass of `RateLimitError` |
|
|
188
|
+
| `APIError` | any other 4xx/5xx, or an unreadable response |
|
|
189
|
+
| `TransportError` | never reached the API: DNS, connection, TLS, timeout (`__cause__` is the httpx error) |
|
|
190
|
+
| `ConfigurationError` | bad client options (raised before any request) |
|
|
191
|
+
|
|
192
|
+
Each carries `.status_code`, `.request_id` and `.body` where available.
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from macadress import Client, RateLimitError, MacadressError
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
r = mac.lookup(value)
|
|
199
|
+
except RateLimitError as exc:
|
|
200
|
+
time.sleep(exc.retry_after or 5)
|
|
201
|
+
except MacadressError as exc:
|
|
202
|
+
log.warning("macadress %s: %s (%s)", exc.status_code, exc, exc.request_id)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Development
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
python -m venv .venv && . .venv/bin/activate
|
|
209
|
+
pip install -e ".[dev]"
|
|
210
|
+
|
|
211
|
+
pytest
|
|
212
|
+
mypy
|
|
213
|
+
ruff check .
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The version lives in `src/macadress/_version.py`; keep `CHANGELOG.md` and the
|
|
217
|
+
release tag in step with it.
|
|
218
|
+
|
|
219
|
+
## Links
|
|
220
|
+
|
|
221
|
+
- API reference: <https://macadress.com/docs>
|
|
222
|
+
- Issues: <https://github.com/sapisos/macadress-python/issues>
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
MIT, see [LICENSE](LICENSE). A product of [ApisOS FZE](https://apisos.com).
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# macadress
|
|
2
|
+
|
|
3
|
+
Official Python client for the [macadress.com](https://macadress.com) MAC address
|
|
4
|
+
and OUI vendor lookup API.
|
|
5
|
+
|
|
6
|
+
- Vendor name, OUI, IEEE block, country, address type, EUI-64 / IPv6 link-local, randomization confidence, device guess
|
|
7
|
+
- Keyless vendor-name lookup, plus keyed single / batch / directory-search calls
|
|
8
|
+
- Typed dataclass results with a `raw` escape hatch, typed exceptions per failure mode
|
|
9
|
+
- Sync `Client` and async `AsyncClient` from the same API, one dependency (`httpx`)
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from macadress import Client
|
|
13
|
+
|
|
14
|
+
mac = Client("mk_live_xxx")
|
|
15
|
+
|
|
16
|
+
mac.vendor("00:03:93:AB:12:34") # "Apple, Inc." (no API key required)
|
|
17
|
+
mac.lookup("00:03:93:AB:12:34").country # "US"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install macadress
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Requires Python 3.10+.
|
|
27
|
+
|
|
28
|
+
## Getting a key
|
|
29
|
+
|
|
30
|
+
`vendor()` needs no key. Everything else does. A free key (1,000 lookups a day) is
|
|
31
|
+
instant at [macadress.com/signup](https://macadress.com/signup); see
|
|
32
|
+
[pricing](https://macadress.com/pricing) for more.
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
### Create a client
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from macadress import Client
|
|
40
|
+
|
|
41
|
+
mac = Client("mk_live_xxx")
|
|
42
|
+
|
|
43
|
+
# keyless: only vendor() will work
|
|
44
|
+
anon = Client()
|
|
45
|
+
|
|
46
|
+
# options
|
|
47
|
+
mac = Client(
|
|
48
|
+
"mk_live_xxx",
|
|
49
|
+
base_url="https://api.macadress.com", # change only for a self-hosted deployment
|
|
50
|
+
timeout=10.0,
|
|
51
|
+
headers={"X-Trace": "my-app"},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# reuse / close
|
|
55
|
+
with Client("mk_live_xxx") as mac:
|
|
56
|
+
...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### `vendor()` - name only, no key
|
|
60
|
+
|
|
61
|
+
Returns `None` when the address is valid but has no vendor to report
|
|
62
|
+
(unregistered, private, or locally administered / randomized).
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
mac.vendor("00:03:93:AB:12:34") # "Apple, Inc."
|
|
66
|
+
mac.vendor("02:1a:2b:3c:4d:5e") # None
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`:`, `-`, `.` and space grouping are all accepted, as is a bare 12-hex string.
|
|
70
|
+
|
|
71
|
+
### `lookup()` - full analysis
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
r = mac.lookup("3C:22:FB:12:34:56")
|
|
75
|
+
|
|
76
|
+
r.organization # str | None
|
|
77
|
+
r.vendor_lookup_reliable # bool (False for a private block / LAA)
|
|
78
|
+
r.oui # "3C:22:FB"
|
|
79
|
+
r.matched_prefix # full matched block at its real width
|
|
80
|
+
r.block_type # BlockType.MA_L (compares equal to "MA-L")
|
|
81
|
+
r.country # "US" | None
|
|
82
|
+
r.administration_type # AdministrationType.UNIVERSALLY_ADMINISTERED | ...
|
|
83
|
+
r.potentially_randomized # bool
|
|
84
|
+
r.randomization_confidence # RandomizationConfidence.NONE | POSSIBLE | LIKELY
|
|
85
|
+
r.eui64 # "3E:22:FB:FF:FE:12:34:56" | None
|
|
86
|
+
r.ipv6_link_local # "fe80::3e22:fbff:fe12:3456" | None
|
|
87
|
+
r.device.category # DeviceCategory.UNKNOWN (usually)
|
|
88
|
+
r.explanation # plain-English summary
|
|
89
|
+
r.meta.database_version # "2026-08-30"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Any field not covered by an attribute is still reachable:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
r.get("vendor_location.city") # dotted path into r.raw, default None
|
|
96
|
+
r.raw # the decoded payload as given
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `batch()` - up to 100 at once
|
|
100
|
+
|
|
101
|
+
Results come back in input order; check each item.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
for item in mac.batch(["00:03:93:00:00:00", "3C:22:FB:00:00:00", "bad"]):
|
|
105
|
+
if item.failed:
|
|
106
|
+
print(item.input, "->", item.error)
|
|
107
|
+
else:
|
|
108
|
+
print(item.input, "->", item.organization)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Raises `ValueError` (no request made) if the iterable is empty or has more than
|
|
112
|
+
`macadress.MAX_BATCH_SIZE` (100) entries.
|
|
113
|
+
|
|
114
|
+
### `search_vendors()` - the directory
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
result = mac.search_vendors("Cisco", country="US", limit=20)
|
|
118
|
+
|
|
119
|
+
result.total # total matches, ignoring the limit
|
|
120
|
+
for block in result:
|
|
121
|
+
print(block.block_type, block.organization, block.country)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `health()`
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
mac.health() # bool, keyless, uncounted; a transport failure is False
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Async
|
|
131
|
+
|
|
132
|
+
`AsyncClient` mirrors `Client` method for method:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import asyncio
|
|
136
|
+
from macadress import AsyncClient
|
|
137
|
+
|
|
138
|
+
async def main():
|
|
139
|
+
async with AsyncClient("mk_live_xxx") as mac:
|
|
140
|
+
print(await mac.vendor("00:03:93:AB:12:34"))
|
|
141
|
+
r = await mac.lookup("3C:22:FB:12:34:56")
|
|
142
|
+
print(r.organization)
|
|
143
|
+
|
|
144
|
+
asyncio.run(main())
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Errors
|
|
148
|
+
|
|
149
|
+
Every failure is a `MacadressError`.
|
|
150
|
+
|
|
151
|
+
| Class | When |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `InvalidMACError` | HTTP 400, the input did not parse |
|
|
154
|
+
| `AuthenticationError` | HTTP 401, missing or invalid API key |
|
|
155
|
+
| `RateLimitError` | HTTP 429, per-minute rate exceeded. `.retry_after` (seconds) when sent |
|
|
156
|
+
| `QuotaExceededError` | HTTP 429, billing-cycle quota spent. Subclass of `RateLimitError` |
|
|
157
|
+
| `APIError` | any other 4xx/5xx, or an unreadable response |
|
|
158
|
+
| `TransportError` | never reached the API: DNS, connection, TLS, timeout (`__cause__` is the httpx error) |
|
|
159
|
+
| `ConfigurationError` | bad client options (raised before any request) |
|
|
160
|
+
|
|
161
|
+
Each carries `.status_code`, `.request_id` and `.body` where available.
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from macadress import Client, RateLimitError, MacadressError
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
r = mac.lookup(value)
|
|
168
|
+
except RateLimitError as exc:
|
|
169
|
+
time.sleep(exc.retry_after or 5)
|
|
170
|
+
except MacadressError as exc:
|
|
171
|
+
log.warning("macadress %s: %s (%s)", exc.status_code, exc, exc.request_id)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Development
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
python -m venv .venv && . .venv/bin/activate
|
|
178
|
+
pip install -e ".[dev]"
|
|
179
|
+
|
|
180
|
+
pytest
|
|
181
|
+
mypy
|
|
182
|
+
ruff check .
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The version lives in `src/macadress/_version.py`; keep `CHANGELOG.md` and the
|
|
186
|
+
release tag in step with it.
|
|
187
|
+
|
|
188
|
+
## Links
|
|
189
|
+
|
|
190
|
+
- API reference: <https://macadress.com/docs>
|
|
191
|
+
- Issues: <https://github.com/sapisos/macadress-python/issues>
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT, see [LICENSE](LICENSE). A product of [ApisOS FZE](https://apisos.com).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Vendor name only, no API key. Run: python examples/01_vendor_name.py 00:03:93:AB:12:34"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from macadress import Client
|
|
6
|
+
|
|
7
|
+
mac = sys.argv[1] if len(sys.argv) > 1 else "00:03:93:AB:12:34"
|
|
8
|
+
|
|
9
|
+
with Client() as client:
|
|
10
|
+
name = client.vendor(mac)
|
|
11
|
+
|
|
12
|
+
print(name if name is not None else f"{mac}: no vendor on record")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Full analysis of one address. Needs a key.
|
|
2
|
+
|
|
3
|
+
Run: MACADRESS_API_KEY=mk_live_xxx python examples/02_full_lookup.py 3C:22:FB:00:00:00
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from macadress import Client
|
|
10
|
+
|
|
11
|
+
mac = sys.argv[1] if len(sys.argv) > 1 else "3C:22:FB:00:00:00"
|
|
12
|
+
|
|
13
|
+
with Client(os.environ.get("MACADRESS_API_KEY", "")) as client:
|
|
14
|
+
r = client.lookup(mac)
|
|
15
|
+
|
|
16
|
+
print("organization:", r.organization)
|
|
17
|
+
print("oui:", r.oui)
|
|
18
|
+
print("block_type:", r.block_type)
|
|
19
|
+
print("country:", r.country)
|
|
20
|
+
print("reliable:", r.vendor_lookup_reliable)
|
|
21
|
+
print("administration:", r.administration_type)
|
|
22
|
+
print("randomization:", r.randomization_confidence)
|
|
23
|
+
print("eui64:", r.eui64)
|
|
24
|
+
print("device:", r.device.category)
|
|
25
|
+
print("database_version:", r.meta.database_version)
|
|
26
|
+
print()
|
|
27
|
+
print(r.explanation)
|
|
28
|
+
|
|
29
|
+
# anything without a typed attribute is still reachable
|
|
30
|
+
print(r.get("vendor_location.city"))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Up to 100 addresses in one request. Needs a key.
|
|
2
|
+
|
|
3
|
+
Run: MACADRESS_API_KEY=mk_live_xxx python examples/03_batch.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
from macadress import Client
|
|
9
|
+
|
|
10
|
+
with Client(os.environ.get("MACADRESS_API_KEY", "")) as client:
|
|
11
|
+
items = client.batch(
|
|
12
|
+
["00:03:93:00:00:00", "3C:22:FB:00:00:00", "AC-DE-48-00:11:22", "not-a-mac"]
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
for item in items:
|
|
16
|
+
if item.failed:
|
|
17
|
+
print(f"{item.input} -> ERROR {item.error}")
|
|
18
|
+
else:
|
|
19
|
+
print(f"{item.input} -> {item.organization or '(no vendor)'}")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Search the registered vendor/block directory. Needs a key.
|
|
2
|
+
|
|
3
|
+
Run: MACADRESS_API_KEY=mk_live_xxx python examples/04_search_vendors.py Cisco US
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from macadress import Client
|
|
10
|
+
|
|
11
|
+
query = sys.argv[1] if len(sys.argv) > 1 else "Cisco"
|
|
12
|
+
country = sys.argv[2] if len(sys.argv) > 2 else None
|
|
13
|
+
|
|
14
|
+
with Client(os.environ.get("MACADRESS_API_KEY", "")) as client:
|
|
15
|
+
result = client.search_vendors(query, country=country, limit=20)
|
|
16
|
+
|
|
17
|
+
print(f"{result.total} total match(es), showing {len(result)}:\n")
|
|
18
|
+
for block in result:
|
|
19
|
+
print(f"{block.block_type or '?'} {block.organization} ({block.country or '?'})")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""AsyncClient mirrors Client. Run: python examples/05_async.py 00:03:93:AB:12:34"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from macadress import AsyncClient, MacadressError, RateLimitError
|
|
8
|
+
|
|
9
|
+
mac = sys.argv[1] if len(sys.argv) > 1 else "00:03:93:AB:12:34"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def main() -> None:
|
|
13
|
+
async with AsyncClient(os.environ.get("MACADRESS_API_KEY", "")) as client:
|
|
14
|
+
name = await client.vendor(mac)
|
|
15
|
+
print(name if name is not None else f"{mac}: no vendor on record")
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
r = await client.lookup(mac)
|
|
19
|
+
except RateLimitError as exc:
|
|
20
|
+
print(f"rate limited, retry after {exc.retry_after or '?'}s")
|
|
21
|
+
except MacadressError as exc:
|
|
22
|
+
print(f"lookup needs a key: {exc}")
|
|
23
|
+
else:
|
|
24
|
+
print("organization:", r.organization)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "macadress"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Official Python client for the macadress.com MAC address and OUI vendor lookup API."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "ApisOS FZE" }]
|
|
13
|
+
keywords = ["macadress", "mac-address", "oui", "vendor-lookup", "mac-vendor", "ieee", "api-client", "sdk"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Internet",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = ["httpx>=0.27"]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://macadress.com"
|
|
30
|
+
Documentation = "https://macadress.com/docs"
|
|
31
|
+
Source = "https://github.com/sapisos/macadress-python"
|
|
32
|
+
Issues = "https://github.com/sapisos/macadress-python/issues"
|
|
33
|
+
Changelog = "https://github.com/sapisos/macadress-python/blob/main/CHANGELOG.md"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
dev = ["pytest>=8", "mypy>=1.11", "ruff>=0.6"]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.version]
|
|
39
|
+
path = "src/macadress/_version.py"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["src/macadress"]
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.sdist]
|
|
45
|
+
include = ["/src", "/tests", "/examples", "/CHANGELOG.md", "/README.md", "/LICENSE"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
line-length = 100
|
|
49
|
+
target-version = "py310"
|
|
50
|
+
src = ["src", "tests"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = ["E", "F", "I", "B", "UP", "W"]
|
|
54
|
+
|
|
55
|
+
[tool.mypy]
|
|
56
|
+
python_version = "3.10"
|
|
57
|
+
strict = true
|
|
58
|
+
files = ["src/macadress"]
|
|
59
|
+
|
|
60
|
+
[tool.pytest.ini_options]
|
|
61
|
+
testpaths = ["tests"]
|