Skip to content

Commit 15c5f89

Browse files
Merge pull request #33 from ipdata/claude/fix-api-key-env-var-j6bTt
Make pytricia optional and add select_field parameter to lookup
2 parents d0e7e48 + 0ec76cf commit 15c5f89

File tree

9 files changed

+76
-324
lines changed

9 files changed

+76
-324
lines changed

.github/workflows/python-publish.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
name: Test and Publish ipdata to PyPI
22

3-
on: push
3+
on:
4+
push:
5+
branches:
6+
- master
7+
tags:
8+
- '*'
49

510
jobs:
611
build-n-publish:

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ Install the latest version of the cli with `pip`.
5757
pip install ipdata
5858
```
5959

60-
or `easy_install`
60+
To use the [IPTrie](#iptrie) data structure, install with the `trie` extra (requires a C compiler):
6161

6262
```bash
63-
easy_install ipdata
63+
pip install ipdata[trie]
6464
```
6565

6666
## Library Usage
@@ -580,6 +580,8 @@ IPTrie is a production-ready, type-safe trie for IP addresses and CIDR prefixes
580580
581581
### Quick Start
582582
583+
> **Note:** IPTrie requires the `trie` extra: `pip install ipdata[trie]`
584+
583585
```python
584586
from ipdata import IPTrie
585587
@@ -741,6 +743,12 @@ A list of possible errors is available at [Status Codes](https://docs.ipdata.co/
741743
742744
## Tests
743745
746+
Install test dependencies:
747+
748+
```shell
749+
pip install ipdata[test,trie]
750+
```
751+
744752
To run all tests
745753
746754
```shell

setup.cfg

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@ install_requires =
2929
click
3030
click_default_group
3131
pyperclip
32+
33+
[options.extras_require]
34+
test =
3235
pytest
3336
hypothesis
3437
python-dotenv
38+
trie =
3539
pytricia
3640

3741
[options.entry_points]

src/ipdata/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,20 @@
77
>>> ipdata.lookup() # or ipdata.lookup("8.8.8.8")
88
"""
99
from .ipdata import IPData
10-
from .iptrie import IPTrie
10+
11+
try:
12+
from .iptrie import IPTrie
13+
except ImportError:
14+
IPTrie = None
1115

1216
# Configuration
1317
api_key = None
1418
endpoint = "https://api.ipdata.co/"
1519
default_client = None
1620

1721

18-
def lookup(resource="", fields=[]):
19-
return _proxy("lookup", resource=resource, fields=fields)
22+
def lookup(resource="", fields=[], select_field=None):
23+
return _proxy("lookup", resource=resource, fields=fields, select_field=select_field)
2024

2125

2226
def bulk(resources, fields=[]):

src/ipdata/cli.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
╰───────────────╯
4747
"""
4848
import csv
49-
import io
5049
import json
5150
import logging
5251
import os
@@ -66,17 +65,16 @@
6665
from rich.progress import Progress
6766
from rich.tree import Tree
6867

69-
from .lolcat import LolCat
7068
from .geofeeds import Geofeed, GeofeedValidationError
71-
from .ipdata import DotDict, IPData
69+
from .ipdata import IPData
7270

7371
console = Console()
7472

7573
FORMAT = "%(message)s"
7674
logging.basicConfig(
7775
level="ERROR", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
7876
)
79-
log = logging.getLogger("rich")
77+
log = logging.getLogger(__name__)
8078

8179
API_KEY_FILE = f"{Path.home()}/.ipdata"
8280

@@ -95,19 +93,24 @@ def _lookup(ipdata, *args, **kwargs):
9593

9694
def print_ascii_logo():
9795
"""
98-
Print cool ascii logo with lolcat.
96+
Print cool ascii logo with rainbow colors.
9997
"""
100-
options = DotDict({"animate": False, "os": 6, "spread": 3.0, "freq": 0.1})
101-
logo = """
102-
_ _ _
103-
(_)_ __ __| | __ _| |_ __ _
98+
from rich.text import Text
99+
100+
logo = r"""
101+
_ _ _
102+
(_)_ __ __| | __ _| |_ __ _
104103
| | '_ \ / _` |/ _` | __/ _` |
105104
| | |_) | (_| | (_| | || (_| |
106105
|_| .__/ \__,_|\__,_|\__\__,_|
107106
|_|
108-
"""
109-
lol = LolCat()
110-
lol.cat(io.StringIO(logo), options)
107+
"""
108+
rainbow = ["red", "orange1", "yellow", "green", "blue", "dark_violet"]
109+
lines = logo.strip("\n").split("\n")
110+
for i, line in enumerate(lines):
111+
text = Text(line)
112+
text.stylize(rainbow[i % len(rainbow)])
113+
console.print(text)
111114

112115

113116
def pretty_print_data(data):

src/ipdata/geofeeds.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,11 @@
99
from pathlib import Path
1010

1111
import requests
12-
from rich.logging import RichHandler
1312

1413
from .codes import COUNTRIES, REGION_CODES
1514

16-
FORMAT = "%(message)s"
17-
logging.basicConfig(
18-
level="ERROR", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
19-
)
20-
log = logging.getLogger("rich")
15+
log = logging.getLogger(__name__)
16+
log.addHandler(logging.NullHandler())
2117

2218
pwd = Path(__file__).parent.resolve()
2319

src/ipdata/ipdata.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,9 @@
2626
import functools
2727

2828
from requests.adapters import HTTPAdapter, Retry
29-
from rich.logging import RichHandler
3029

31-
FORMAT = "%(message)s"
32-
logging.basicConfig(
33-
level="ERROR", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
34-
)
30+
logger = logging.getLogger(__name__)
31+
logger.addHandler(logging.NullHandler())
3532

3633

3734
class IPDataException(Exception):
@@ -68,7 +65,7 @@ class IPData(object):
6865
:param debug: A boolean used to set the log level. Set to True when debugging.
6966
"""
7067

71-
log = logging.getLogger("rich")
68+
log = logging.getLogger(__name__)
7269

7370
valid_fields = {
7471
"ip",
@@ -100,13 +97,15 @@ class IPData(object):
10097

10198
def __init__(
10299
self,
103-
api_key=os.environ.get("IPDATA_API_KEY"),
100+
api_key=None,
104101
endpoint="https://api.ipdata.co/",
105102
timeout=60,
106103
retry_limit=7,
107104
retry_backoff_factor=1,
108105
debug=False,
109106
):
107+
if api_key is None:
108+
api_key = os.environ.get("IPDATA_API_KEY")
110109
if not api_key:
111110
raise IPDataException("API Key not set. Set an API key via the 'IPDATA_API_KEY' environment variable or see the docs for other ways to do so.")
112111
# Request settings
@@ -119,6 +118,13 @@ def __init__(
119118
# Enable debugging
120119
if debug:
121120
self.log.setLevel(logging.DEBUG)
121+
if not any(
122+
not isinstance(h, logging.NullHandler) for h in self.log.handlers
123+
):
124+
from rich.logging import RichHandler
125+
handler = RichHandler()
126+
handler.setFormatter(logging.Formatter("%(message)s", datefmt="[%X]"))
127+
self.log.addHandler(handler)
122128

123129
# Work around renamed argument in urllib3.
124130
if hasattr(urllib3.util.Retry.DEFAULT, "allowed_methods"):
@@ -167,17 +173,18 @@ def _validate_ip_address(self, ip):
167173
if request_ip.is_private or request_ip.is_reserved or request_ip.is_multicast:
168174
raise ValueError(f"{ip} is a reserved IP Address")
169175

170-
def lookup(self, resource="", fields=[]):
176+
def lookup(self, resource="", fields=[], select_field=None):
171177
"""
172178
Makes a GET request to the IPData API for the specified 'resource' and the given 'fields'.
173179
174180
:param resource: Either an IP address or an ASN prefixed by "AS" eg. "AS15169"
175181
:param fields: A collection of API fields to be returned
182+
:param select_field: A single field name to return (convenience alternative to fields)
176183
177184
:returns: An API response as a DotDict object to allow dot notation access of fields eg. data.ip, data.company.name, data.threat.blocklists[0].name etc
178185
179186
:raises IPDataException: if the API call fails or if there is a failure in decoding the response.
180-
:raises ValueError: if 'resource' is not a string
187+
:raises ValueError: if 'resource' is not a string, or if both 'select_field' and 'fields' are provided
181188
"""
182189
if type(resource) is not str:
183190
raise ValueError(f"{resource} must be of type 'str'")
@@ -189,6 +196,13 @@ def lookup(self, resource="", fields=[]):
189196
if resource and not resource.startswith("AS"):
190197
self._validate_ip_address(resource)
191198

199+
if select_field is not None:
200+
if fields:
201+
raise ValueError("'select_field' and 'fields' are mutually exclusive. Use one or the other.")
202+
if not isinstance(select_field, str):
203+
raise ValueError("'select_field' must be of type 'str'.")
204+
fields = [select_field]
205+
192206
self._validate_fields(fields)
193207
query_params = self._query_params | {"fields": ",".join(fields)}
194208

src/ipdata/iptrie.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
from collections.abc import Iterator
2121
from typing import TypeVar, Generic
2222

23-
from pytricia import PyTricia
23+
try:
24+
from pytricia import PyTricia
25+
except ImportError:
26+
PyTricia = None
2427

2528
T = TypeVar("T")
2629

@@ -94,6 +97,11 @@ class IPTrie(Generic[T]):
9497

9598
def __init__(self) -> None:
9699
"""Initialize an empty IPTrie."""
100+
if PyTricia is None:
101+
raise ImportError(
102+
"pytricia is required for IPTrie but is not installed. "
103+
"Install it with: pip install ipdata[trie]"
104+
)
97105
self._ipv4: PyTricia = PyTricia(self.IPV4_BITS)
98106
self._ipv6: PyTricia = PyTricia(self.IPV6_BITS)
99107

0 commit comments

Comments
 (0)