Skip to content

Commit 0cd403a

Browse files
Introduce functions for user agent parsing
1 parent d5e9333 commit 0cd403a

File tree

8 files changed

+216
-42
lines changed

8 files changed

+216
-42
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99
### Changed
1010
- Require Python 3.8+.
11+
- Add new functions for user-agent header value parsing: `batch_parse_user_agents`, `parse_user_agent`.
1112
- API key is passed as header value and no longer as query parameter.
1213
- Client library method are now wrapped in a new _ApiResponse_ object that includes a mean to retrieve metadata
1314
about _credits_ and _throttling_ in addition to _data_.

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@ response = client.origin_lookup_ip()
6363
print(response.data)
6464
```
6565

66+
#### User-Agent Parsing
67+
68+
```python
69+
from ipregistry import IpregistryClient
70+
71+
client = IpregistryClient("YOUR_API_KEY")
72+
response = client.parse_user_agent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36')
73+
print(response.data)
74+
```
75+
6676
More advanced examples are available in the [samples](https://github.com/ipregistry/ipregistry-python/tree/master/samples)
6777
folder.
6878

@@ -108,9 +118,9 @@ A manner to proceed is to identify bots using the `User-Agent` header.
108118
To ease this process, the library includes a utility method:
109119

110120
```python
111-
from ipregistry import UserAgent
121+
from ipregistry import UserAgents
112122

113-
is_bot = UserAgent.is_bot('YOUR_USER_AGENT_HEADER_VALUE_HERE')
123+
is_bot = UserAgents.is_bot('YOUR_USER_AGENT_HEADER_VALUE_HERE')
114124
```
115125

116126
## Other Libraries

ipregistry/core.py

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,40 +31,46 @@ def __init__(self, key_or_config, **kwargs):
3131
raise ValueError("Given request handler instance is not of type IpregistryRequestHandler")
3232

3333
def batch_lookup_ips(self, ips, **options):
34-
sparse_cache = [None] * len(ips)
34+
return self.batch_request(ips, self._requestHandler.batch_lookup_ips, **options)
35+
36+
def batch_parse_user_agents(self, user_agents, **options):
37+
return self.batch_request(user_agents, self._requestHandler.batch_parse_user_agents, **options)
38+
39+
def batch_request(self, items, request_handler_func, **options):
40+
sparse_cache = [None] * len(items)
3541
cache_misses = []
3642

37-
for i in range(0, len(ips)):
38-
ip = ips[i]
39-
cache_key = self.__build_cache_key(ip, options)
43+
for i in range(len(items)):
44+
item = items[i]
45+
cache_key = self.__build_cache_key(item, options)
4046
cache_value = self._cache.get(cache_key)
4147
if cache_value is None:
42-
cache_misses.append(ip)
48+
cache_misses.append(item)
4349
else:
4450
sparse_cache[i] = cache_value
4551

46-
result = [None] * len(ips)
52+
result = [None] * len(items)
4753
if len(cache_misses) > 0:
48-
response = self._requestHandler.batch_lookup_ips(cache_misses, options)
54+
response = request_handler_func(cache_misses, options)
4955
else:
5056
response = ApiResponse(
5157
ApiResponseCredits(),
5258
[],
5359
ApiResponseThrottling()
5460
)
5561

56-
fresh_ip_info = response.data
62+
fresh_item_info = response.data
5763
j = 0
5864
k = 0
5965

60-
for cached_ip_info in sparse_cache:
61-
if cached_ip_info is None:
62-
if not isinstance(fresh_ip_info[k], LookupError):
63-
self._cache.put(self.__build_cache_key(ips[j], options), fresh_ip_info[k])
64-
result[j] = fresh_ip_info[k]
66+
for cached_item_info in sparse_cache:
67+
if cached_item_info is None:
68+
if not isinstance(fresh_item_info[k], LookupError):
69+
self._cache.put(self.__build_cache_key(items[j], options), fresh_item_info[k])
70+
result[j] = fresh_item_info[k]
6571
k += 1
6672
else:
67-
result[j] = cached_ip_info
73+
result[j] = cached_item_info
6874
j += 1
6975

7076
response.data = result
@@ -80,6 +86,9 @@ def lookup_ip(self, ip='', **options):
8086
def origin_lookup_ip(self, **options):
8187
return self.__lookup_ip('', options)
8288

89+
def origin_parse_user_agent(self, **options):
90+
return self._requestHandler.origin_parse_user_agent(options)
91+
8392
def __lookup_ip(self, ip, options):
8493
cache_key = self.__build_cache_key(ip, options)
8594
cache_value = self._cache.get(cache_key)
@@ -96,9 +105,14 @@ def __lookup_ip(self, ip, options):
96105
ApiResponseThrottling()
97106
)
98107

108+
def parse_user_agent(self, user_agent, **options):
109+
response = self.batch_parse_user_agents([user_agent], **options)
110+
response.data = response.data[0]
111+
return response
112+
99113
@staticmethod
100-
def __build_cache_key(ip, options):
101-
result = ip
114+
def __build_cache_key(key, options):
115+
result = key
102116

103117
for key, value in options.items():
104118
if isinstance(value, bool):

ipregistry/json.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,10 @@ class IpInfo(BaseModel):
267267
class RequesterAutonomousSystem(AutonomousSystem):
268268
pass
269269

270-
271270
class RequesterIpInfo(IpInfo):
272271
user_agent: Optional[UserAgent] = None
273272

274-
model_config = ConfigDict(extra='ignore')
273+
model_config = ConfigDict(extra='ignore')
274+
275+
class RequesterUserAgent(UserAgent):
276+
pass

ipregistry/request.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
from .__init__ import __version__
2626
from .model import (ApiError, ApiResponse, ApiResponseCredits, ApiResponseThrottling, ClientError, IpInfo,
27-
LookupError, RequesterIpInfo)
27+
LookupError, RequesterIpInfo, RequesterUserAgent, UserAgent)
2828

2929

3030
class IpregistryRequestHandler(ABC):
@@ -35,6 +35,10 @@ def __init__(self, config):
3535
def batch_lookup_ips(self, ips, options):
3636
pass
3737

38+
@abstractmethod
39+
def batch_parse_user_agents(self, user_agents, options):
40+
pass
41+
3842
@abstractmethod
3943
def lookup_ip(self, ip, options):
4044
pass
@@ -43,8 +47,12 @@ def lookup_ip(self, ip, options):
4347
def origin_lookup_ip(self, options):
4448
pass
4549

46-
def _build_base_url(self, ip, options):
47-
result = self._config.base_url + "/" + ip
50+
@abstractmethod
51+
def origin_parse_user_agent(self, options):
52+
pass
53+
54+
def _build_base_url(self, resource, options):
55+
result = self._config.base_url + "/" + resource
4856

4957
i = 0
5058
for key, value in options.items():
@@ -80,6 +88,29 @@ def batch_lookup_ips(self, ips, options):
8088
except Exception as e:
8189
raise ClientError(e)
8290

91+
def batch_parse_user_agents(self, user_agents, options):
92+
response = None
93+
try:
94+
response = requests.post(
95+
self._build_base_url('user_agent', options),
96+
data=json.dumps(user_agents),
97+
headers=self.__headers(),
98+
timeout=self._config.timeout
99+
)
100+
response.raise_for_status()
101+
results = response.json().get('results', [])
102+
103+
parsed_results = [
104+
LookupError(data) if 'code' in data else UserAgent(**data)
105+
for data in results
106+
]
107+
108+
return self.build_api_response(response, parsed_results)
109+
except requests.HTTPError:
110+
self.__create_api_error(response)
111+
except Exception as e:
112+
raise ClientError(e)
113+
83114
def lookup_ip(self, ip, options):
84115
response = None
85116
try:
@@ -104,6 +135,26 @@ def lookup_ip(self, ip, options):
104135
def origin_lookup_ip(self, options):
105136
return self.lookup_ip('', options)
106137

138+
def origin_parse_user_agent(self, options):
139+
response = None
140+
try:
141+
response = requests.get(
142+
self._build_base_url('user_agent', options),
143+
headers=self.__headers(),
144+
timeout=self._config.timeout
145+
)
146+
response.raise_for_status()
147+
json_response = response.json()
148+
149+
return self.build_api_response(
150+
response,
151+
RequesterUserAgent(**json_response)
152+
)
153+
except requests.HTTPError:
154+
self.__create_api_error(response)
155+
except Exception as err:
156+
raise ClientError(err)
157+
107158
@staticmethod
108159
def build_api_response(response, data):
109160
throttling_limit = DefaultRequestHandler.__convert_to_int(response.headers.get('x-rate-limit-limit'))

samples/batch-parse-user-agents.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""
2+
Copyright 2019 Ipregistry (https://ipregistry.co).
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
"""
16+
17+
from ipregistry import ApiError, ClientError, IpregistryClient
18+
19+
try:
20+
api_key = "tryout"
21+
client = IpregistryClient(api_key)
22+
response = client.batch_parse_user_agents([
23+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
24+
'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36'
25+
])
26+
print(response.data)
27+
except ApiError as e:
28+
print("API error", e)
29+
except ClientError as e:
30+
print("Client error", e)
31+
except Exception as e:
32+
print("Unexpected error", e)

samples/parse-user-agent.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""
2+
Copyright 2019 Ipregistry (https://ipregistry.co).
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
"""
16+
17+
from ipregistry import ApiError, ClientError, IpregistryClient
18+
19+
try:
20+
api_key = "tryout"
21+
client = IpregistryClient(api_key)
22+
response = client.parse_user_agent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36')
23+
print(response.data)
24+
except ApiError as e:
25+
print("API error", e)
26+
except ClientError as e:
27+
print("Client error", e)
28+
except Exception as e:
29+
print("Unexpected error", e)

0 commit comments

Comments
 (0)