Compare commits

...

2 commits

Author SHA1 Message Date
ee31311db7 Replace development tools
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/tests Pipeline was successful
2025-01-05 17:11:44 +01:00
780dd5a624 Replace dig usage 2025-01-05 17:08:28 +01:00
5 changed files with 275 additions and 311 deletions

View file

@ -9,14 +9,12 @@ dependencies = [
'click>=8.0.1', 'click>=8.0.1',
'python-dotenv>=0.15.0', 'python-dotenv>=0.15.0',
'requests>=2.25.1', 'requests>=2.25.1',
'cryptography>=3.4.7' 'cryptography>=3.4.7',
] ]
[project.optional-dependencies] [project.optional-dependencies]
development = [ development = [
'black>=20.8b1', "ruff>=0.8.6",
'isort>=5.6.4',
'autoflake>=1.4',
] ]
ci = ['coverage>=5.3.1'] ci = ['coverage>=5.3.1']

View file

@ -3,8 +3,7 @@ import click
from transip_client.main import detect from transip_client.main import detect
DEFAULT_DNS = "myip.opendns.com" DEFAULT_SERVICE = "https://api.ipify.org"
DEFAULT_DNS_NAME = "@resolver1.opendns.com"
DEFAULT_API_URL = "https://api.transip.nl/v6" DEFAULT_API_URL = "https://api.transip.nl/v6"
@ -13,11 +12,18 @@ DEFAULT_API_URL = "https://api.transip.nl/v6"
@click.option("--token", envvar="TOKEN") @click.option("--token", envvar="TOKEN")
@click.option("--login", envvar="LOGIN") @click.option("--login", envvar="LOGIN")
@click.option("--private-key-path", envvar="PRIVATE_KEY_PATH") @click.option("--private-key-path", envvar="PRIVATE_KEY_PATH")
@click.option("--dns", envvar="DNS", default=DEFAULT_DNS) @click.option("--service", envvar="SERVICE", default=DEFAULT_SERVICE)
@click.option("--dns-name", envvar="DNS_NAME", default=DEFAULT_DNS_NAME)
@click.option("--api-url", envvar="API_URL", default=DEFAULT_API_URL) @click.option("--api-url", envvar="API_URL", default=DEFAULT_API_URL)
@click.option("--read-only/--write", envvar="READ_ONLY", default=False) @click.option("--read-only/--write", envvar="READ_ONLY", default=False)
def run(domains, token, login, private_key_path, dns, dns_name, api_url, read_only): def run(
domains: list[str],
token: str,
login: str,
private_key_path: str,
service: str,
api_url: str,
read_only: bool,
) -> None:
if not domains: if not domains:
raise ValueError("No domain(s) specified") raise ValueError("No domain(s) specified")
@ -40,7 +46,7 @@ def run(domains, token, login, private_key_path, dns, dns_name, api_url, read_on
detect( detect(
domains, domains,
(dns, dns_name), service,
(private_key_path, login), (private_key_path, login),
token, token,
api_url, api_url,

View file

@ -1,10 +1,10 @@
import base64 import base64
import json import json
import logging import logging
import subprocess
import time import time
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Generator
import requests import requests
@ -17,19 +17,17 @@ from cryptography.hazmat.primitives.hashes import SHA512
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _get_ip(resolvers): def _get_ip(service: str) -> str:
try: try:
output = subprocess.check_output( response = requests.get(service, timeout=10)
["dig", "+short", *resolvers], response.raise_for_status()
stderr=subprocess.STDOUT, except requests.RequestException as e:
) raise OSError(f"Unable to retrieve current IP from {service}") from e
except subprocess.CalledProcessError as e:
raise OSError("Unable to retrieve current IP") from e
return output.decode("utf-8").strip() return response.text
def _get_token(private_key_path, login, api_url): def _get_token(private_key_path: str, login: str, api_url: str) -> str:
request = requests.Request( request = requests.Request(
"POST", "POST",
f"{api_url}/auth", f"{api_url}/auth",
@ -66,13 +64,14 @@ def _get_token(private_key_path, login, api_url):
return response_data["token"] return response_data["token"]
def _get_domain(domain, token, api_url): def _get_domain(domain: str, token: str, api_url: str) -> requests.Response:
headers = {"Authorization": f"Bearer {token}"} headers = {"Authorization": f"Bearer {token}"}
return requests.get(f"{api_url}/domains/{domain}/dns", headers=headers) return requests.get(f"{api_url}/domains/{domain}/dns", headers=headers)
def _get_domain_data(domains, token, api_url): def _get_domain_data(
domains: list[str], token: str, api_url: str
) -> Generator[dict, None, None]:
with ThreadPoolExecutor(max_workers=10) as executor: with ThreadPoolExecutor(max_workers=10) as executor:
futures = { futures = {
executor.submit(_get_domain, domain, token, api_url): domain executor.submit(_get_domain, domain, token, api_url): domain
@ -80,19 +79,21 @@ def _get_domain_data(domains, token, api_url):
} }
for future in as_completed(futures): for future in as_completed(futures):
response = future.result()
domain = futures[future] domain = futures[future]
try: try:
response = future.result()
response.raise_for_status() response.raise_for_status()
except requests.HTTPError as e: except requests.HTTPError:
logger.exception(f"Failed retrieving information for {domain}") logger.exception(f"Failed retrieving information for {domain}")
continue continue
yield {"domain": domain, **response.json()} yield {"domain": domain, **response.json()}
def _update_domain(domain, payload, api_url, token): def _update_domain(
domain: str, payload: dict, api_url: str, token: str
) -> requests.Response:
headers = {"Authorization": f"Bearer {token}"} headers = {"Authorization": f"Bearer {token}"}
return requests.put( return requests.put(
@ -100,7 +101,9 @@ def _update_domain(domain, payload, api_url, token):
) )
def _update_domains(updated_domains, api_url, token, read_only): def _update_domains(
updated_domains: dict, api_url: str, token: str, read_only: bool
) -> None:
if read_only: if read_only:
return return
@ -123,14 +126,21 @@ def _update_domains(updated_domains, api_url, token, read_only):
logger.info(f"Updated domain {domain}") logger.info(f"Updated domain {domain}")
def detect(domains, resolvers, credentials, token, api_url, read_only): def detect(
ip = _get_ip(resolvers) domains: list[str],
updated_domains = {} service: str,
credentials: tuple[str, str],
token: str,
api_url: str,
read_only: bool,
) -> None:
ip = _get_ip(service)
if all(credentials): if all(credentials):
token = _get_token(*credentials, api_url) token = _get_token(*credentials, api_url)
domain_data = _get_domain_data(domains, token, api_url) domain_data = _get_domain_data(domains, token, api_url)
updated_domains = {}
for data in domain_data: for data in domain_data:
dns_entries = data["dnsEntries"] dns_entries = data["dnsEntries"]

View file

@ -1,21 +1,18 @@
import json import json
import os import os
from unittest import TestCase from unittest import TestCase, skip
from unittest.mock import call, patch from unittest.mock import call, patch, Mock
from pathlib import Path from pathlib import Path
from click.testing import CliRunner from click.testing import CliRunner
from requests import HTTPError from requests import HTTPError
from transip_client.cli import DEFAULT_API_URL, run from transip_client.cli import DEFAULT_API_URL, DEFAULT_SERVICE, run
class RunTestCase(TestCase): class RunTestCase(TestCase):
def setUp(self): def setUp(self):
patcher = patch("transip_client.main.subprocess.check_output")
self.mocked_dns = patcher.start()
patcher = patch("transip_client.main.requests.get") patcher = patch("transip_client.main.requests.get")
self.mocked_get = patcher.start() self.mocked_get = patcher.start()
@ -28,30 +25,36 @@ class RunTestCase(TestCase):
self.runner = CliRunner() self.runner = CliRunner()
def test_simple(self): def test_simple(self):
self.mocked_dns.return_value = b"111.420\n" self.mocked_get.side_effect = [
self.mocked_get.return_value.json.return_value = { Mock(text="111.420"),
"dnsEntries": [ Mock(
{ json=lambda: {
"name": "@", "dnsEntries": [
"expire": 60, {
"type": "A", "name": "@",
"content": "111.421", "expire": 60,
"type": "A",
"content": "111.421",
}
],
"_links": [
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foobar.com",
},
],
} }
], ),
"_links": [ ]
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foobar.com",
},
],
}
with self.assertLogs("transip_client.main", level="INFO") as logger: with self.assertLogs("transip_client.main", level="INFO") as logger:
result = self.runner.invoke(run, ["foobar.com"], env={"TOKEN": "token"}) result = self.runner.invoke(
run, "foobar.com", env={"TOKEN": "token"}, catch_exceptions=False
)
self.assertEqual( self.assertEqual(
logger.output, ["INFO:transip_client.main:Updated domain foobar.com"] logger.output, ["INFO:transip_client.main:Updated domain foobar.com"]
@ -84,11 +87,12 @@ class RunTestCase(TestCase):
) )
def test_error_response(self): def test_error_response(self):
self.mocked_dns.return_value = b"111.420\n" self.mocked_get.side_effect = [Mock(text="111.420"), HTTPError]
self.mocked_get.return_value.raise_for_status.side_effect = HTTPError
with self.assertLogs("transip_client.main", level="INFO") as logger: with self.assertLogs("transip_client.main", level="INFO") as logger:
result = self.runner.invoke(run, ["foobar.com"], env={"TOKEN": "token"}) result = self.runner.invoke(
run, ["foobar.com"], env={"TOKEN": "token"}, catch_exceptions=False
)
error_log = logger.output[0] error_log = logger.output[0]
@ -103,27 +107,31 @@ class RunTestCase(TestCase):
self.mocked_put.assert_not_called() self.mocked_put.assert_not_called()
def test_matching_ip(self): def test_matching_ip(self):
self.mocked_dns.return_value = b"111.420\n" self.mocked_get.side_effect = [
self.mocked_get.return_value.json.return_value = { Mock(text="111.420"),
"dnsEntries": [ Mock(
{ json=lambda: {
"name": "@", "dnsEntries": [
"expire": 60, {
"type": "A", "name": "@",
"content": "111.420", "expire": 60,
"type": "A",
"content": "111.420",
}
],
"_links": [
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foobar.com",
},
],
} }
], ),
"_links": [ ]
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foobar.com",
},
],
}
result = self.runner.invoke(run, ["foobar.com"], env={"TOKEN": "token"}) result = self.runner.invoke(run, ["foobar.com"], env={"TOKEN": "token"})
@ -137,27 +145,31 @@ class RunTestCase(TestCase):
self.mocked_put.assert_not_called() self.mocked_put.assert_not_called()
def test_readonly(self): def test_readonly(self):
self.mocked_dns.return_value = b"111.420\n" self.mocked_get.side_effect = [
self.mocked_get.return_value.json.return_value = { Mock(text="111.420"),
"dnsEntries": [ Mock(
{ json=lambda: {
"name": "@", "dnsEntries": [
"expire": 60, {
"type": "A", "name": "@",
"content": "111.421", "expire": 60,
"type": "A",
"content": "111.421",
}
],
"_links": [
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foobar.com",
},
],
} }
], ),
"_links": [ ]
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foobar.com",
},
],
}
result = self.runner.invoke( result = self.runner.invoke(
run, ["foobar.com", "--read-only"], env={"TOKEN": "token"} run, ["foobar.com", "--read-only"], env={"TOKEN": "token"}
@ -173,27 +185,31 @@ class RunTestCase(TestCase):
self.mocked_put.assert_not_called() self.mocked_put.assert_not_called()
def test_different_api_url(self): def test_different_api_url(self):
self.mocked_dns.return_value = b"111.420\n" self.mocked_get.side_effect = [
self.mocked_get.return_value.json.return_value = { Mock(text="111.420"),
"dnsEntries": [ Mock(
{ json=lambda: {
"name": "@", "dnsEntries": [
"expire": 60, {
"type": "A", "name": "@",
"content": "111.421", "expire": 60,
"type": "A",
"content": "111.421",
}
],
"_links": [
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foobar.com",
},
],
} }
], ),
"_links": [ ]
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foobar.com",
},
],
}
with self.assertLogs("transip_client.main", level="INFO") as logger: with self.assertLogs("transip_client.main", level="INFO") as logger:
result = self.runner.invoke( result = self.runner.invoke(
@ -233,27 +249,31 @@ class RunTestCase(TestCase):
) )
def test_env_var(self): def test_env_var(self):
self.mocked_dns.return_value = b"111.420\n" self.mocked_get.side_effect = [
self.mocked_get.return_value.json.return_value = { Mock(text="111.420"),
"dnsEntries": [ Mock(
{ json=lambda: {
"name": "@", "dnsEntries": [
"expire": 60, {
"type": "A", "name": "@",
"content": "111.421", "expire": 60,
"type": "A",
"content": "111.421",
}
],
"_links": [
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foobar.com",
},
],
} }
], ),
"_links": [ ]
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foobar.com",
},
],
}
with self.assertLogs("transip_client.main", level="INFO") as logger: with self.assertLogs("transip_client.main", level="INFO") as logger:
result = self.runner.invoke( result = self.runner.invoke(
@ -296,48 +316,52 @@ class RunTestCase(TestCase):
) )
def test_multi_arg_env_var(self): def test_multi_arg_env_var(self):
self.mocked_dns.return_value = b"111.420\n" self.mocked_get.side_effect = [
self.mocked_get.return_value.json.side_effect = [ Mock(text="111.420"),
{ Mock(
"dnsEntries": [ json=lambda: {
{ "dnsEntries": [
"name": "@", {
"expire": 60, "name": "@",
"type": "A", "expire": 60,
"content": "111.421", "type": "A",
} "content": "111.421",
], }
"_links": [ ],
{ "_links": [
"rel": "self", {
"link": "https://api.transip.nl/v6/domains/foobar.com/dns", "rel": "self",
}, "link": "https://api.transip.nl/v6/domains/foobar.com/dns",
{ },
"rel": "domain", {
"link": "https://api.transip.nl/v6/domains/foobar.com", "rel": "domain",
}, "link": "https://api.transip.nl/v6/domains/foobar.com",
], },
}, ],
{ },
"dnsEntries": [ ),
{ Mock(
"name": "@", json=lambda: {
"expire": 60, "dnsEntries": [
"type": "A", {
"content": "111.421", "name": "@",
} "expire": 60,
], "type": "A",
"_links": [ "content": "111.421",
{ }
"rel": "self", ],
"link": "https://api.transip.nl/v6/domains/foofoo.com/dns", "_links": [
}, {
{ "rel": "self",
"rel": "domain", "link": "https://api.transip.nl/v6/domains/foofoo.com/dns",
"link": "https://api.transip.nl/v6/domains/foofoo.com", },
}, {
], "rel": "domain",
}, "link": "https://api.transip.nl/v6/domains/foofoo.com",
},
],
}
),
] ]
with self.assertLogs("transip_client.main", level="INFO") as logger: with self.assertLogs("transip_client.main", level="INFO") as logger:
@ -357,18 +381,15 @@ class RunTestCase(TestCase):
self.assertEqual(result.exit_code, 0) self.assertEqual(result.exit_code, 0)
expected_calls = [ expected_calls = [
call(DEFAULT_SERVICE, timeout=10),
call( call(
f"{DEFAULT_API_URL}/domains/foobar.com/dns", f"{DEFAULT_API_URL}/domains/foobar.com/dns",
headers={"Authorization": "Bearer token"}, headers={"Authorization": "Bearer token"},
), ),
call().raise_for_status(),
call().json(),
call( call(
f"{DEFAULT_API_URL}/domains/foofoo.com/dns", f"{DEFAULT_API_URL}/domains/foofoo.com/dns",
headers={"Authorization": "Bearer token"}, headers={"Authorization": "Bearer token"},
), ),
call().raise_for_status(),
call().json(),
] ]
# use any_order because of the asynchronous requests # use any_order because of the asynchronous requests
@ -393,13 +414,11 @@ class RunTestCase(TestCase):
data=expected_json, data=expected_json,
headers={"Authorization": "Bearer token"}, headers={"Authorization": "Bearer token"},
), ),
call().raise_for_status(),
call( call(
f"{DEFAULT_API_URL}/domains/foofoo.com/dns", f"{DEFAULT_API_URL}/domains/foofoo.com/dns",
data=expected_json, data=expected_json,
headers={"Authorization": "Bearer token"}, headers={"Authorization": "Bearer token"},
), ),
call().raise_for_status(),
] ]
self.mocked_put.assert_has_calls(expected_calls, any_order=True) self.mocked_put.assert_has_calls(expected_calls, any_order=True)
@ -420,7 +439,7 @@ class RunTestCase(TestCase):
self.assertEqual( self.assertEqual(
str(result.exception), str(result.exception),
"Either a token or a login name with a path to a private key need" "Either a token or a login name with a path to a private key need"
" to be specified" " to be specified",
) )
self.mocked_get.assert_not_called() self.mocked_get.assert_not_called()
@ -432,34 +451,38 @@ class RunTestCase(TestCase):
self.assertEqual(result.exit_code, 1) self.assertEqual(result.exit_code, 1)
self.assertEqual( self.assertEqual(
str(result.exception), str(result.exception),
"Both a login name and the path to a private key need to be specified" "Both a login name and the path to a private key need to be specified",
) )
self.mocked_get.assert_not_called() self.mocked_get.assert_not_called()
self.mocked_put.assert_not_called() self.mocked_put.assert_not_called()
def test_login_with_private_key_path(self): def test_login_with_private_key_path(self):
self.mocked_dns.return_value = b"111.420\n" self.mocked_get.side_effect = [
self.mocked_get.return_value.json.return_value = { Mock(text="111.420"),
"dnsEntries": [ Mock(
{ json=lambda: {
"name": "@", "dnsEntries": [
"expire": 60, {
"type": "A", "name": "@",
"content": "111.421", "expire": 60,
"type": "A",
"content": "111.421",
}
],
"_links": [
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foobar.com",
},
],
} }
], ),
"_links": [ ]
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foobar.com",
},
],
}
self.mocked_session.return_value.json.return_value = {"token": "FOOBAR"} self.mocked_session.return_value.json.return_value = {"token": "FOOBAR"}
@ -471,7 +494,7 @@ class RunTestCase(TestCase):
result = self.runner.invoke( result = self.runner.invoke(
run, run,
["foobar.com"], ["foobar.com"],
env={"LOGIN": "foo", "PRIVATE_KEY_PATH": str(private_key_path)} env={"LOGIN": "foo", "PRIVATE_KEY_PATH": str(private_key_path)},
) )
self.assertEqual( self.assertEqual(

127
uv.lock generated
View file

@ -1,46 +1,6 @@
version = 1 version = 1
requires-python = ">=3.11" requires-python = ">=3.11"
[[package]]
name = "autoflake"
version = "2.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyflakes" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2a/cb/486f912d6171bc5748c311a2984a301f4e2d054833a1da78485866c71522/autoflake-2.3.1.tar.gz", hash = "sha256:c98b75dc5b0a86459c4f01a1d32ac7eb4338ec4317a4469515ff1e687ecd909e", size = 27642 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/ee/3fd29bf416eb4f1c5579cf12bf393ae954099258abd7bde03c4f9716ef6b/autoflake-2.3.1-py3-none-any.whl", hash = "sha256:3ae7495db9084b7b32818b4140e6dc4fc280b712fb414f5b8fe57b0a8e85a840", size = 32483 },
]
[[package]]
name = "black"
version = "24.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "mypy-extensions" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "platformdirs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d8/0d/cc2fb42b8c50d80143221515dd7e4766995bd07c56c9a3ed30baf080b6dc/black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", size = 645813 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/cc/7496bb63a9b06a954d3d0ac9fe7a73f3bf1cd92d7a58877c27f4ad1e9d41/black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad", size = 1607468 },
{ url = "https://files.pythonhosted.org/packages/2b/e3/69a738fb5ba18b5422f50b4f143544c664d7da40f09c13969b2fd52900e0/black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50", size = 1437270 },
{ url = "https://files.pythonhosted.org/packages/c9/9b/2db8045b45844665c720dcfe292fdaf2e49825810c0103e1191515fc101a/black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392", size = 1737061 },
{ url = "https://files.pythonhosted.org/packages/a3/95/17d4a09a5be5f8c65aa4a361444d95edc45def0de887810f508d3f65db7a/black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175", size = 1423293 },
{ url = "https://files.pythonhosted.org/packages/90/04/bf74c71f592bcd761610bbf67e23e6a3cff824780761f536512437f1e655/black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3", size = 1644256 },
{ url = "https://files.pythonhosted.org/packages/4c/ea/a77bab4cf1887f4b2e0bce5516ea0b3ff7d04ba96af21d65024629afedb6/black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65", size = 1448534 },
{ url = "https://files.pythonhosted.org/packages/4e/3e/443ef8bc1fbda78e61f79157f303893f3fddf19ca3c8989b163eb3469a12/black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f", size = 1761892 },
{ url = "https://files.pythonhosted.org/packages/52/93/eac95ff229049a6901bc84fec6908a5124b8a0b7c26ea766b3b8a5debd22/black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8", size = 1434796 },
{ url = "https://files.pythonhosted.org/packages/d0/a0/a993f58d4ecfba035e61fca4e9f64a2ecae838fc9f33ab798c62173ed75c/black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", size = 1643986 },
{ url = "https://files.pythonhosted.org/packages/37/d5/602d0ef5dfcace3fb4f79c436762f130abd9ee8d950fa2abdbf8bbc555e0/black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", size = 1448085 },
{ url = "https://files.pythonhosted.org/packages/47/6d/a3a239e938960df1a662b93d6230d4f3e9b4a22982d060fc38c42f45a56b/black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", size = 1760928 },
{ url = "https://files.pythonhosted.org/packages/dd/cf/af018e13b0eddfb434df4d9cd1b2b7892bab119f7a20123e93f6910982e8/black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", size = 1436875 },
{ url = "https://files.pythonhosted.org/packages/8d/a7/4b27c50537ebca8bec139b872861f9d2bf501c5ec51fcf897cb924d9e264/black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", size = 206898 },
]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2024.12.14" version = "2024.12.14"
@ -252,51 +212,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
] ]
[[package]]
name = "isort"
version = "5.13.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/87/f9/c1eb8635a24e87ade2efce21e3ce8cd6b8630bb685ddc9cdaca1349b2eb5/isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", size = 175303 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310 },
]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
]
[[package]]
name = "packaging"
version = "24.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
]
[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
]
[[package]]
name = "platformdirs"
version = "4.3.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
]
[[package]] [[package]]
name = "pycparser" name = "pycparser"
version = "2.22" version = "2.22"
@ -306,15 +221,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
] ]
[[package]]
name = "pyflakes"
version = "3.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/f9/669d8c9c86613c9d568757c7f5824bd3197d7b1c6c27553bc5618a27cce2/pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", size = 63788 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725 },
]
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.0.1" version = "1.0.1"
@ -339,6 +245,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
] ]
[[package]]
name = "ruff"
version = "0.8.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/da/00/089db7890ea3be5709e3ece6e46408d6f1e876026ec3fd081ee585fef209/ruff-0.8.6.tar.gz", hash = "sha256:dcad24b81b62650b0eb8814f576fc65cfee8674772a6e24c9b747911801eeaa5", size = 3473116 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/28/aa07903694637c2fa394a9f4fe93cf861ad8b09f1282fa650ef07ff9fe97/ruff-0.8.6-py3-none-linux_armv6l.whl", hash = "sha256:defed167955d42c68b407e8f2e6f56ba52520e790aba4ca707a9c88619e580e3", size = 10628735 },
{ url = "https://files.pythonhosted.org/packages/2b/43/827bb1448f1fcb0fb42e9c6edf8fb067ca8244923bf0ddf12b7bf949065c/ruff-0.8.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:54799ca3d67ae5e0b7a7ac234baa657a9c1784b48ec954a094da7c206e0365b1", size = 10386758 },
{ url = "https://files.pythonhosted.org/packages/df/93/fc852a81c3cd315b14676db3b8327d2bb2d7508649ad60bfdb966d60738d/ruff-0.8.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e88b8f6d901477c41559ba540beeb5a671e14cd29ebd5683903572f4b40a9807", size = 10007808 },
{ url = "https://files.pythonhosted.org/packages/94/e9/e0ed4af1794335fb280c4fac180f2bf40f6a3b859cae93a5a3ada27325ae/ruff-0.8.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0509e8da430228236a18a677fcdb0c1f102dd26d5520f71f79b094963322ed25", size = 10861031 },
{ url = "https://files.pythonhosted.org/packages/82/68/da0db02f5ecb2ce912c2bef2aa9fcb8915c31e9bc363969cfaaddbc4c1c2/ruff-0.8.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a7ddb221779871cf226100e677b5ea38c2d54e9e2c8ed847450ebbdf99b32d", size = 10388246 },
{ url = "https://files.pythonhosted.org/packages/ac/1d/b85383db181639019b50eb277c2ee48f9f5168f4f7c287376f2b6e2a6dc2/ruff-0.8.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:248b1fb3f739d01d528cc50b35ee9c4812aa58cc5935998e776bf8ed5b251e75", size = 11424693 },
{ url = "https://files.pythonhosted.org/packages/ac/b7/30bc78a37648d31bfc7ba7105b108cb9091cd925f249aa533038ebc5a96f/ruff-0.8.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:bc3c083c50390cf69e7e1b5a5a7303898966be973664ec0c4a4acea82c1d4315", size = 12141921 },
{ url = "https://files.pythonhosted.org/packages/60/b3/ee0a14cf6a1fbd6965b601c88d5625d250b97caf0534181e151504498f86/ruff-0.8.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52d587092ab8df308635762386f45f4638badb0866355b2b86760f6d3c076188", size = 11692419 },
{ url = "https://files.pythonhosted.org/packages/ef/d6/c597062b2931ba3e3861e80bd2b147ca12b3370afc3889af46f29209037f/ruff-0.8.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61323159cf21bc3897674e5adb27cd9e7700bab6b84de40d7be28c3d46dc67cf", size = 12981648 },
{ url = "https://files.pythonhosted.org/packages/68/84/21f578c2a4144917985f1f4011171aeff94ab18dfa5303ac632da2f9af36/ruff-0.8.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ae4478b1471fc0c44ed52a6fb787e641a2ac58b1c1f91763bafbc2faddc5117", size = 11251801 },
{ url = "https://files.pythonhosted.org/packages/6c/aa/1ac02537c8edeb13e0955b5db86b5c050a1dcba54f6d49ab567decaa59c1/ruff-0.8.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0c000a471d519b3e6cfc9c6680025d923b4ca140ce3e4612d1a2ef58e11f11fe", size = 10849857 },
{ url = "https://files.pythonhosted.org/packages/eb/00/020cb222252d833956cb3b07e0e40c9d4b984fbb2dc3923075c8f944497d/ruff-0.8.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9257aa841e9e8d9b727423086f0fa9a86b6b420fbf4bf9e1465d1250ce8e4d8d", size = 10470852 },
{ url = "https://files.pythonhosted.org/packages/00/56/e6d6578202a0141cd52299fe5acb38b2d873565f4670c7a5373b637cf58d/ruff-0.8.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45a56f61b24682f6f6709636949ae8cc82ae229d8d773b4c76c09ec83964a95a", size = 10972997 },
{ url = "https://files.pythonhosted.org/packages/be/31/dd0db1f4796bda30dea7592f106f3a67a8f00bcd3a50df889fbac58e2786/ruff-0.8.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:496dd38a53aa173481a7d8866bcd6451bd934d06976a2505028a50583e001b76", size = 11317760 },
{ url = "https://files.pythonhosted.org/packages/d4/70/cfcb693dc294e034c6fed837fa2ec98b27cc97a26db5d049345364f504bf/ruff-0.8.6-py3-none-win32.whl", hash = "sha256:e169ea1b9eae61c99b257dc83b9ee6c76f89042752cb2d83486a7d6e48e8f764", size = 8799729 },
{ url = "https://files.pythonhosted.org/packages/60/22/ae6bcaa0edc83af42751bd193138bfb7598b2990939d3e40494d6c00698c/ruff-0.8.6-py3-none-win_amd64.whl", hash = "sha256:f1d70bef3d16fdc897ee290d7d20da3cbe4e26349f62e8a0274e7a3f4ce7a905", size = 9673857 },
{ url = "https://files.pythonhosted.org/packages/91/f8/3765e053acd07baa055c96b2065c7fab91f911b3c076dfea71006666f5b0/ruff-0.8.6-py3-none-win_arm64.whl", hash = "sha256:7d7fc2377a04b6e04ffe588caad613d0c460eb2ecba4c0ccbbfe2bc973cbc162", size = 9149556 },
]
[[package]] [[package]]
name = "sentry-sdk" name = "sentry-sdk"
version = "2.19.2" version = "2.19.2"
@ -368,9 +299,7 @@ ci = [
{ name = "coverage" }, { name = "coverage" },
] ]
development = [ development = [
{ name = "autoflake" }, { name = "ruff" },
{ name = "black" },
{ name = "isort" },
] ]
sentry-enabled = [ sentry-enabled = [
{ name = "sentry-sdk" }, { name = "sentry-sdk" },
@ -378,14 +307,12 @@ sentry-enabled = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "autoflake", marker = "extra == 'development'", specifier = ">=1.4" },
{ name = "black", marker = "extra == 'development'", specifier = ">=20.8b1" },
{ name = "click", specifier = ">=8.0.1" }, { name = "click", specifier = ">=8.0.1" },
{ name = "coverage", marker = "extra == 'ci'", specifier = ">=5.3.1" }, { name = "coverage", marker = "extra == 'ci'", specifier = ">=5.3.1" },
{ name = "cryptography", specifier = ">=3.4.7" }, { name = "cryptography", specifier = ">=3.4.7" },
{ name = "isort", marker = "extra == 'development'", specifier = ">=5.6.4" },
{ name = "python-dotenv", specifier = ">=0.15.0" }, { name = "python-dotenv", specifier = ">=0.15.0" },
{ name = "requests", specifier = ">=2.25.1" }, { name = "requests", specifier = ">=2.25.1" },
{ name = "ruff", marker = "extra == 'development'", specifier = ">=0.8.6" },
{ name = "sentry-sdk", marker = "extra == 'sentry-enabled'", specifier = ">=0.19.5" }, { name = "sentry-sdk", marker = "extra == 'sentry-enabled'", specifier = ">=0.19.5" },
] ]