Compare commits

..

5 Commits

15 changed files with 129 additions and 279 deletions

View File

@@ -1,21 +0,0 @@
__pycache__
*.pyc
*.pyo
*.pyd
.Python
.env
.venv
.git
.gitignore
*.sqlite3
*.db
analysis.py
db.py
pyproject.toml
README.md
run_speedtest.py
uv.lock
.dockerignore
.python-version
speedtest.db
Dockerfile

View File

@@ -1 +0,0 @@
3.13

View File

@@ -0,0 +1,28 @@
# speed-logger
Runs periodic internet speed tests using the Cloudflare speed test API and visualizes the results in Grafana. Tests run every 15 minutes and record download speed, upload speed, latency, and jitter. Results are stored in a SQLite database and displayed on a pre-configured Grafana dashboard.
![Dashboard](res/dashboard.png)
## Requirements
- Docker and Docker Compose
## Deployment
Create a `.env` file in the project root with the following variables:
```
GRAFANA_PORT=3000
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=changeme
```
Start the containers:
```
docker compose up -d
```
Grafana will be available at `http://localhost:3000`. The dashboard is provisioned automatically on first start.

View File

@@ -3,12 +3,10 @@ services:
build: ./measurement
container_name: speedtest
restart: unless-stopped
network_mode: host # needed for get_mac_address ARP lookup
volumes:
- sqlite-data:/data
environment:
- DB_PATH=/data/speedtest.db
- ROUTER_MAC=${ROUTER_MAC}
grafana:
image: grafana/grafana:latest
@@ -21,8 +19,10 @@ services:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
- GF_INSTALL_PLUGINS=frser-sqlite-datasource
volumes:
- sqlite-data:/data:ro
- ./provisioning:/etc/grafana/provisioning
- sqlite-data:/data
- ./grafana/provisioning:/etc/grafana/provisioning
volumes:
sqlite-data:
external: true
name: speed-logger_sqlite-data

View File

@@ -31,7 +31,7 @@
},
"id": 100,
"panels": [],
"title": "📊 Summary",
"title": "Summary",
"type": "row"
},
{
@@ -482,7 +482,7 @@
},
"id": 101,
"panels": [],
"title": "📈 Speed Over Time",
"title": "Speed Over Time",
"type": "row"
},
{
@@ -860,7 +860,7 @@
},
"id": 102,
"panels": [],
"title": "🕐 Time-of-Day & Weekday Patterns",
"title": "Time-of-Day & Weekday Patterns",
"type": "row"
},
{
@@ -1261,7 +1261,7 @@
},
"id": 103,
"panels": [],
"title": "🔴 Uptime & Failures",
"title": "Uptime & Failures",
"type": "row"
},
{
@@ -1400,7 +1400,7 @@
},
"id": 104,
"panels": [],
"title": "🗃️ Raw Data",
"title": "Raw Data",
"type": "row"
},
{

View File

@@ -7,5 +7,5 @@ datasources:
access: proxy
isDefault: true
jsonData:
path: /var/lib/grafana/data/speedtest.db
path: /data/speedtest.db
editable: true

View File

@@ -1,14 +1,20 @@
FROM python:3.12-slim
RUN apt-get update && apt-get install -y cron && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
# Install cron, curl, and the official Ookla Speedtest repository
RUN apt-get update && apt-get install -y cron curl \
&& curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | bash \
&& apt-get install -y speedtest \
&& rm -rf /var/lib/apt/lists/*
COPY . .
COPY crontab /etc/cron.d/speedtest
RUN chmod 0644 /etc/cron.d/speedtest && crontab /etc/cron.d/speedtest
# Set up the cron job
COPY crontab /etc/cron.d/speedtest-cron
RUN chmod 0644 /etc/cron.d/speedtest-cron
RUN crontab /etc/cron.d/speedtest-cron
CMD ["cron", "-f"]
ENV DB_PATH=/app/speedtests.db
# Dump env vars for cron and start the daemon
CMD printenv > /etc/environment && cron -f

View File

@@ -1,3 +1,4 @@
import os
DB_PATH = os.get_env("DB_PATH")
DB_PATH = os.getenv("DB_PATH") or "speedtest.db"

View File

@@ -1 +1,2 @@
*/15 * * * * cd /app && python run_speedtest.py >> /proc/1/fd/1 2>> /proc/1/fd/2
*/15 * * * * cd /app && /usr/local/bin/python3 run_speedtest.py >> /proc/1/fd/1 2>> /proc/1/fd/2
# Empty line required at the end!

View File

@@ -1,7 +1,7 @@
import sqlite3
import time
from config import DB_PATH
def init_db(db_path=DB_PATH):
conn = sqlite3.connect(db_path)
c = conn.cursor()
@@ -17,73 +17,48 @@ def init_db(db_path=DB_PATH):
location_region TEXT,
latency REAL,
jitter REAL,
down_100kB REAL,
down_1MB REAL,
down_10MB REAL,
down_25MB REAL,
down_90th REAL,
up_100kB REAL,
up_1MB REAL,
up_10MB REAL,
up_90th REAL
down_100kB REAL, down_1MB REAL, down_10MB REAL, down_25MB REAL, down_90th REAL,
up_100kB REAL, up_1MB REAL, up_10MB REAL, up_90th REAL
)
''')
conn.commit()
conn.close()
def insert_result(results: dict|None, db_path=DB_PATH):
def insert_result(data: dict|None, db_path=DB_PATH):
conn = sqlite3.connect(db_path)
c = conn.cursor()
# If the test failed entirely, store it as a failure with timestamp now
if results is None or "tests" not in results:
from time import time
c.execute("INSERT INTO speed_tests (timestamp, failed) VALUES (?, ?)", (time(), True))
if data is None:
c.execute("INSERT INTO speed_tests (timestamp, failed) VALUES (?, ?)", (time.time(), True))
conn.commit()
conn.close()
return
tests = results.get("tests", {})
meta = results.get("meta", {})
# Convert bytes per second to Megabits per second (Mbps)
def to_mbps(bytes_per_sec):
return (bytes_per_sec * 8) / 1000000 if bytes_per_sec else None
# Get a consistent timestamp from any TestResult (or fallback to now)
from time import time as now
sample_test = next(iter(tests.values()), None)
timestamp = sample_test.time if sample_test else now()
print(tests)
print(meta)
def get(tests, key):
return tests[key].value if key in tests else None
# Ookla provides a UTC ISO timestamp, but we'll stick to local Unix time for consistency with your old data
current_time = time.time()
c.execute('''
INSERT INTO speed_tests (
timestamp, failed, isp, ip, location_code, location_city, location_region,
latency, jitter,
down_100kB, down_1MB, down_10MB, down_25MB, down_90th,
up_100kB, up_1MB, up_10MB, up_90th
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
latency, jitter, down_90th, up_90th
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
timestamp, False,
get(tests, "isp"),
get(meta, "ip"),
get(meta, "location_code"),
get(meta, "location_city"),
get(meta, "location_region"),
get(tests, "latency"),
get(tests, "jitter"),
get(tests, "100kB_down_mbps"),
get(tests, "1MB_down_mbps"),
get(tests, "10MB_down_mbps"),
get(tests, "25MB_down_mbps"),
get(tests, "90th_percentile_down_mbps"),
get(tests, "100kB_up_mbps"),
get(tests, "1MB_up_mbps"),
get(tests, "10MB_up_mbps"),
get(tests, "90th_percentile_up_mbps")
current_time,
False,
data.get("isp"),
data.get("interface", {}).get("externalIp"),
data.get("server", {}).get("name"), # Ookla Server Name
data.get("server", {}).get("location"), # Ookla Server City
data.get("server", {}).get("country"), # Ookla Server Country
data.get("ping", {}).get("latency"),
data.get("ping", {}).get("jitter"),
to_mbps(data.get("download", {}).get("bandwidth")),
to_mbps(data.get("upload", {}).get("bandwidth"))
))
conn.commit()
conn.close()

View File

@@ -1,45 +0,0 @@
# This file was autogenerated by uv via the following command:
# uv export --format requirements-txt -o requirements.txt --no-hashes
blinker==1.9.0
# via flask
click==8.3.1
# via flask
colorama==0.4.6 ; sys_platform == 'win32'
# via click
flask==3.1.2
# via speed-logger
getmac==0.9.5
# via speed-logger
gunicorn==23.0.0
# via speed-logger
itsdangerous==2.2.0
# via flask
jinja2==3.1.6
# via flask
markupsafe==3.0.3
# via
# flask
# jinja2
# werkzeug
numpy==2.4.0
# via pandas
packaging==25.0
# via gunicorn
pandas==2.3.3
# via speed-logger
python-dateutil==2.9.0.post0
# via pandas
pytz==2025.2
# via
# pandas
# speed-logger
six==1.17.0
# via python-dateutil
tzdata==2025.3
# via pandas
werkzeug==3.1.4
# via flask
sqlalchemy==2.0.46
# via flask-sqlalchemy
flask-sqlalchemy==3.1.1
# via speed-logger

View File

@@ -1,16 +1,53 @@
from cfspeedtest import CloudflareSpeedtest
import subprocess
import json
import traceback
from db import init_db, insert_result
def run_test_and_save():
try:
tester = CloudflareSpeedtest()
results = tester.run_all(megabits=True) # returns SuiteResults
except Exception:
results = None # Trigger a failed test record
print("==== Running Ookla Speedtest ====")
init_db()
insert_result(results)
try:
# Corrected the typo in --accept-license
result = subprocess.run(
["speedtest", "--accept-license", "--accept-gdpr", "-f", "json"],
capture_output=True, text=True, check=True
)
# Ookla sometimes outputs logs and results in the same stream.
# We need to isolate the actual result JSON.
output_str = result.stdout.strip()
final_data = None
# Split by newline in case they are separated, or scan the string
for line in output_str.split('\n'):
if '{"type":"result"' in line:
# Isolate the valid JSON starting from {"type":"result"
start_idx = line.find('{"type":"result"')
valid_json_str = line[start_idx:]
try:
final_data = json.loads(valid_json_str)
break
except json.JSONDecodeError:
pass
if final_data and "download" in final_data:
insert_result(final_data)
print(f"Results successfully saved. Down: {final_data['download']['bandwidth'] * 8 / 1000000:.2f} Mbps")
else:
print("Could not find valid result JSON in output:", output_str)
insert_result(None)
except subprocess.CalledProcessError as e:
print("Speedtest failed execution:")
print("STDOUT:", e.stdout)
print("STDERR:", e.stderr)
insert_result(None)
except Exception as e:
print("Error processing results:")
print(traceback.format_exc())
insert_result(None)
print("==== Running Speedtest ====")
run_test_and_save()
print("==== Speedtest ended ====")
if __name__ == "__main__":
run_test_and_save()

View File

@@ -1,13 +0,0 @@
[project]
name = "speed-logger"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"cloudflarepycli",
"getmac>=0.9.5",
]
[tool.uv.sources]
cloudflarepycli = { git = "https://github.com/cato447/cloudflarepycli" }

BIN
res/dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 KiB

118
uv.lock generated
View File

@@ -1,118 +0,0 @@
version = 1
revision = 2
requires-python = ">=3.13"
[[package]]
name = "certifi"
version = "2025.11.12"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
[[package]]
name = "cloudflarepycli"
version = "2.1.0"
source = { git = "https://github.com/cato447/cloudflarepycli#26477fb5511839648a774f1685426925b5b44311" }
dependencies = [
{ name = "requests" },
]
[[package]]
name = "getmac"
version = "0.9.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/89/a8/4af8e06912cd83b1cc6493e9b5d0589276c858f7bdccaf1855df748983de/getmac-0.9.5.tar.gz", hash = "sha256:456435cdbf1f5f45c433a250b8b795146e893b6fc659060f15451e812a2ab17d", size = 94031, upload-time = "2024-07-16T01:47:05.642Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/85/4cdbc925381422397bd2b3280680e130091173f2c8dfafb9216eaaa91b00/getmac-0.9.5-py2.py3-none-any.whl", hash = "sha256:22b8a3e15bc0c6bfa94651a3f7f6cd91b59432e1d8199411d4fe12804423e0aa", size = 35781, upload-time = "2024-07-16T01:47:03.75Z" },
]
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "requests"
version = "2.32.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
[[package]]
name = "speed-logger"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "cloudflarepycli" },
{ name = "getmac" },
]
[package.metadata]
requires-dist = [
{ name = "cloudflarepycli", git = "https://github.com/cato447/cloudflarepycli" },
{ name = "getmac", specifier = ">=0.9.5" },
]
[[package]]
name = "urllib3"
version = "2.6.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" },
]