Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add regctl #58

Merged
merged 29 commits into from
Sep 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
## Unreleased


## 1.6.0
### Added
- add regctl normalization schema


## 1.5.2
### Fixed
- issue introduced in 1.5.1 that could cause some contact points to be lost
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Unreleased:
deprecated: []
removed: []
security: []
1.6.0:
added:
- add regctl normalization schema
1.5.2:
fixed:
- issue introduced in 1.5.1 that could cause some contact points to be lost
Expand Down
2 changes: 1 addition & 1 deletion Ctl/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.5.2
1.6.0
2 changes: 1 addition & 1 deletion Ctl/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ ctl:
sign: true

- name: version
type: version
type: semver2
config:
branch_dev: main
branch_release: main
Expand Down
116 changes: 116 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# API

## Retrieve objects

```python
import rdap

# instantiate client
client = rdap.RdapClient()

# asn
as63311 = client.get_asn(63311)

# domain
domain = client.get_domain('example.com')

# ip
ip = client.get_ip('206.41.110.0')

# entity
entity = client.get_entity('NETWO7047-ARIN')
```

## Output normalized data (ASN example)

```python
import json
import rdap

# instantiate client
client = rdap.RdapClient()

# request asn
as63311 = client.get_asn(63311)

# normalized dict
print(json.dumps(as63311.normalized, indent=2))
```

Output:
```json
{
"created": "2014-11-17T14:28:43-05:00",
"updated": "2018-10-24T22:58:16-04:00",
"asn": 63311,
"name": "20C",
"organization": {
"name": "20C, LLC"
},
"locations": [
{
"updated": "2014-08-05T15:21:11-04:00",
"country": null,
"city": null,
"postal_code": null,
"address": "303 W Ohio #1701\nChicago\nIL\n60654\nUnited States",
"geo": null,
"floor": null,
"suite": null
},
{
"updated": "2023-08-02T14:15:09-04:00",
"country": null,
"city": null,
"postal_code": null,
"address": "603 Discovery Dr\nWest Chicago\nIL\n60185\nUnited States",
"geo": null,
"floor": null,
"suite": null
}
],
"contacts": [
{
"created": "2014-07-03T23:22:49-04:00",
"updated": "2023-08-02T14:15:09-04:00",
"name": "Network Engineers",
"roles": [
"abuse",
"admin",
"technical"
],
"phone": "+1 978-636-0020",
"email": "neteng@20c.com"
}
],
"sources": [
{
"created": "2014-11-17T14:28:43-05:00",
"updated": "2018-10-24T22:58:16-04:00",
"handle": "AS63311",
"urls": [
"https://rdap.org/autnum/63311",
"https://rdap.arin.net/registry/autnum/63311"
],
"description": null
}
]
}
```

## Work with normalized data through pydantic models

```python
import rdap

from rdap.schema.normalized import Network

# instantiate client
client = rdap.RdapClient()

# request asn
as63311 = Network(**client.get_asn(63311).normalized)

for contact in as63311.contacts:
print(contact.name, contact.email)
```
946 changes: 537 additions & 409 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ rdap = "rdap.cli:main"
python = "^3.8"
requests = ">=2.25.1"
munge = {version = ">=1.3", extras = ["yaml"]}

pydantic = ">=2.8.2"
googlemaps = ">=4.10"
phonenumbers = ">=8.13.0"

[tool.poetry.dev-dependencies]
# testing
Expand Down
21 changes: 14 additions & 7 deletions rdap/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import rdap
from rdap.config import Config
from rdap.context import RdapRequestContext


def add_options(parser, options):
Expand Down Expand Up @@ -49,6 +50,9 @@
parser.add_argument(
"--parse", action="store_true", help="parse data into object before display"
)
parser.add_argument(
"--normalize", action="store_true", help="normalize data before display"
)
parser.add_argument("--rir", action="store_true", help="display rir", default=False)
parser.add_argument(
"--write-bootstrap-data",
Expand All @@ -75,13 +79,16 @@

codec = munge.get_codec(output_format)()
for each in argd["query"]:
obj = client.get(each)
if argd.get("rir", False):
print(f"rir: {obj.get_rir()}")
if argd.get("parse", False):
print(codec.dumps(obj.parsed()))
else:
print(codec.dumps(obj.data))
with RdapRequestContext(client=client):
obj = client.get(each)
if argd.get("rir", False):
print(f"rir: {obj.get_rir()}")

Check warning on line 85 in rdap/cli.py

View check run for this annotation

Codecov / codecov/patch

rdap/cli.py#L85

Added line #L85 was not covered by tests
if argd.get("parse", False):
print(codec.dumps(obj.parsed()))

Check warning on line 87 in rdap/cli.py

View check run for this annotation

Codecov / codecov/patch

rdap/cli.py#L87

Added line #L87 was not covered by tests
elif argd.get("normalize", False):
print(codec.dumps(obj.normalized))

Check warning on line 89 in rdap/cli.py

View check run for this annotation

Codecov / codecov/patch

rdap/cli.py#L89

Added line #L89 was not covered by tests
else:
print(codec.dumps(obj.data))

if argd.get("show_requests", False):
print("# Requests")
Expand Down
32 changes: 19 additions & 13 deletions rdap/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import rdap
import rdap.bootstrap
from rdap.config import Config
from rdap.context import RdapRequestContext
from rdap.exceptions import RdapHTTPError, RdapNotFoundError
from rdap.objects import RdapAsn, RdapDomain, RdapEntity, RdapNetwork

Expand Down Expand Up @@ -129,20 +130,23 @@
)

def _get(self, url):
res = self.http.get(url, timeout=self.timeout)
for redir in res.history:
self._history.append((strip_auth(redir.url), redir.status_code))
self._history.append((strip_auth(res.url), res.status_code))
with RdapRequestContext(url) as ctx:
res = self.http.get(url, timeout=self.timeout)
for redir in res.history:
self._history.append((strip_auth(redir.url), redir.status_code))
self._history.append((strip_auth(res.url), res.status_code))

if res.status_code == 200:
return res
ctx.push_url(strip_auth(res.url))

msg = "RDAP lookup to {} returned {}".format(
strip_auth(res.url), res.status_code
)
if res.status_code == 404:
raise RdapNotFoundError(msg)
raise RdapHTTPError(msg)
if res.status_code == 200:
return res

msg = "RDAP lookup to {} returned {}".format(
strip_auth(res.url), res.status_code
)
if res.status_code == 404:
raise RdapNotFoundError(msg)
raise RdapHTTPError(msg)

Check warning on line 149 in rdap/client.py

View check run for this annotation

Codecov / codecov/patch

rdap/client.py#L149

Added line #L149 was not covered by tests

def asn_url(self, asn):
"""Gets the correct url for specified ASN."""
Expand Down Expand Up @@ -243,6 +247,8 @@
if qstr.startswith("as"):
return self.get_asn(qstr[2:])

return self.get_entity(qstr, self.url)

Check warning on line 250 in rdap/client.py

View check run for this annotation

Codecov / codecov/patch

rdap/client.py#L250

Added line #L250 was not covered by tests

raise NotImplementedError(f"unknown query {query}")

@lru_cache(maxsize=1024)
Expand Down Expand Up @@ -315,7 +321,7 @@
query = f"/ip/{address}"
return RdapNetwork(self._rdap_get(query).json(), self)

def get_entity(self, handle, base_url):
def get_entity(self, handle, base_url=None):
"""
get entity information in object form
"""
Expand Down
130 changes: 130 additions & 0 deletions rdap/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""
Contact management for RDAP requests.
"""

from contextvars import ContextVar
from datetime import datetime

import pydantic

from rdap.exceptions import RdapHTTPError

__all__ = [
"rdap_request",
"RdapRequestContext",
"RdapRequestState",
]


class RdapSource(pydantic.BaseModel):
"""
Describes a source of RDAP data.
"""

# urls requested for this source
urls: list[str] = pydantic.Field(default_factory=list)

# rdap object handle
handle: str | None = None

# source creation date (if available)
created: datetime | None = None

# source last update date (if available)
updated: datetime | None = None


class RdapRequestState(pydantic.BaseModel):
"""
Describe the current rdap request, tracking sources queried
and entities retrieved.
"""

# list of sources for the current request
sources: list[RdapSource] = pydantic.Field(default_factory=list)

# reference to the rdap client instance
client: object | None = None

# cache of entities (to avoid duplicate requests to the same entity
# within the current request context)
entities: dict = pydantic.Field(default_factory=dict)

def update_source(
self, handle: str, created: datetime | None, updated: datetime | None
):
"""
Update the current source with the handle and dates.
"""

self.sources[-1].handle = handle
self.sources[-1].created = created
self.sources[-1].updated = updated


# context that holds the currently requested rdap url

rdap_request = ContextVar("rdap_request", default=RdapRequestState())

# context manager to set the rdap url
# can be nested


class RdapRequestContext:
"""
Opens a request context

If no state is present, a new state is created.

If a state is present, a new source is added to the state.
"""

def __init__(self, url: str = None, client: object = None):
self.url = url
self.token = None
self.client = client

def __enter__(self):

# get existing state

state = rdap_request.get()

if state and self.url:
state.sources.append(RdapSource(urls=[self.url]))
else:
state = RdapRequestState(
sources=[RdapSource(urls=[self.url] if self.url else [])]
)
self.token = rdap_request.set(state)

if self.client:
state.client = self.client

return self

def __exit__(self, *exc):
if self.token:
rdap_request.reset(self.token)

def push_url(self, url: str):
state = rdap_request.get()
state.sources[-1].urls.append(url)

def get(self, typ: str, handle: str):
state = rdap_request.get()
client = state.client

Check warning on line 116 in rdap/context.py

View check run for this annotation

Codecov / codecov/patch

rdap/context.py#L115-L116

Added lines #L115 - L116 were not covered by tests

if typ not in ["entity", "ip", "domain", "autnum"]:
raise ValueError(f"Invalid type: {typ}")

Check warning on line 119 in rdap/context.py

View check run for this annotation

Codecov / codecov/patch

rdap/context.py#L118-L119

Added lines #L118 - L119 were not covered by tests

if state.entities.get(handle):
return state.entities[handle]
try:
get = getattr(client, f"get_{typ}")
r_entity = get(handle).normalized
state.entities[handle] = r_entity
return r_entity
except RdapHTTPError:
state.entities[handle] = {}
return {}

Check warning on line 130 in rdap/context.py

View check run for this annotation

Codecov / codecov/patch

rdap/context.py#L121-L130

Added lines #L121 - L130 were not covered by tests
Loading
Loading