Compare commits
5 Commits
77c1615675
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 138d905938 | |||
| 914f2ad2c9 | |||
| 90194076a6 | |||
| 176ae47ece | |||
| aa9373496c |
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
3.13
|
||||
28
README.md
28
README.md
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,5 +7,5 @@ datasources:
|
||||
access: proxy
|
||||
isDefault: true
|
||||
jsonData:
|
||||
path: /var/lib/grafana/data/speedtest.db
|
||||
path: /data/speedtest.db
|
||||
editable: true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
|
||||
DB_PATH = os.get_env("DB_PATH")
|
||||
DB_PATH = os.getenv("DB_PATH") or "speedtest.db"
|
||||
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
print("==== Running Speedtest ====")
|
||||
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)
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_test_and_save()
|
||||
print("==== Speedtest ended ====")
|
||||
|
||||
@@ -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
BIN
res/dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 566 KiB |
118
uv.lock
generated
118
uv.lock
generated
@@ -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" },
|
||||
]
|
||||
Reference in New Issue
Block a user