Compare commits
54 Commits
feat/termi
...
a78d999a61
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a78d999a61 | ||
|
|
74afdc1b5b | ||
|
|
607fdd4d46 | ||
|
|
ca01b96e80 | ||
|
467faff592
|
|||
|
3791d0be6e
|
|||
|
06526ca92d
|
|||
|
e1ff6e42a5
|
|||
|
a6670f6533
|
|||
|
598cea1ac8
|
|||
|
9b4febeddb
|
|||
|
c15a5d5a8b
|
|||
|
720e59c144
|
|||
|
c45a30675c
|
|||
|
9e8e23165e
|
|||
|
53e05922bf
|
|||
|
0be0dad1b2
|
|||
|
f404d55935
|
|||
|
009c2b430c
|
|||
|
ed96fbcc29
|
|||
|
4555ef5da2
|
|||
|
1335a73eb6
|
|||
|
b8f3039629
|
|||
|
1888160fa5
|
|||
|
7dd0f839cf
|
|||
|
5a0068586a
|
|||
|
8079780c08
|
|||
|
72b8dae35e
|
|||
|
323ace5775
|
|||
|
c2803e372a
|
|||
|
2a9e704f29
|
|||
|
0c490625a9
|
|||
|
b9753617ad
|
|||
|
b87d19c5d9
|
|||
|
67e8b4cf7e
|
|||
|
bfc6652f29
|
|||
|
38372c0cff
|
|||
|
dd64313006
|
|||
|
9e20a6171a
|
|||
|
da347fd860
|
|||
|
776b7de753
|
|||
|
7b2b3659bb
|
|||
|
872373dffd
|
|||
|
8d832372cd
|
|||
|
03dae87272
|
|||
|
4c654fcb78
|
|||
|
c9542e4af7
|
|||
|
e184375897
|
|||
|
844f1b52e2
|
|||
|
19c51c3665
|
|||
|
85ebd460ed
|
|||
|
50879b4f0e
|
|||
|
6c09923281
|
|||
|
332c408b89
|
33
.dockerignore
Normal file
33
.dockerignore
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Bytecode and virtualenvs
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.venv/
|
||||||
|
.vscode/
|
||||||
|
.vs/
|
||||||
|
.ruff_check/
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Pycache in subdirectories
|
||||||
|
**/__pycache__/
|
||||||
|
**/*.pyc
|
||||||
|
**/*.pyo
|
||||||
|
|
||||||
|
# Git and CI
|
||||||
|
.git/
|
||||||
|
.gitea/
|
||||||
|
testing/
|
||||||
|
tests/
|
||||||
|
|
||||||
|
# Build and docs
|
||||||
|
Dockerfile
|
||||||
|
NathanWoodburn.bsdesign
|
||||||
|
LICENSE.txt
|
||||||
|
README.md
|
||||||
|
|
||||||
|
|
||||||
|
# Development caches
|
||||||
|
*.tmp
|
||||||
|
*.log
|
||||||
|
|
||||||
|
|
||||||
18
.gitea/workflows/check.yml
Normal file
18
.gitea/workflows/check.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
name: Check Code Quality
|
||||||
|
run-name: Ruff CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
RuffCheck:
|
||||||
|
runs-on: [ubuntu-latest, amd]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python
|
||||||
|
run: |
|
||||||
|
apt update
|
||||||
|
apt install -y python3 python3-pip
|
||||||
|
- name: Install Ruff
|
||||||
|
run: pip install ruff
|
||||||
|
- name: Run Ruff
|
||||||
|
run: ruff check .
|
||||||
16
.pre-commit-config.yaml
Normal file
16
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/astral-sh/uv-pre-commit
|
||||||
|
# uv version.
|
||||||
|
rev: 0.9.8
|
||||||
|
hooks:
|
||||||
|
- id: uv-lock
|
||||||
|
- id: uv-export
|
||||||
|
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
# Ruff version.
|
||||||
|
rev: v0.14.4
|
||||||
|
hooks:
|
||||||
|
# Run the linter.
|
||||||
|
- id: ruff-check
|
||||||
|
# Run the formatter.
|
||||||
|
- id: ruff-format
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
66
Dockerfile
66
Dockerfile
@@ -1,18 +1,62 @@
|
|||||||
FROM --platform=$BUILDPLATFORM python:3.13-alpine AS builder
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
### Build stage ###
|
||||||
|
FROM python:3.13-alpine AS build
|
||||||
|
|
||||||
|
# Install build dependencies for Pillow and other native wheels
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
build-base \
|
||||||
|
jpeg-dev zlib-dev freetype-dev
|
||||||
|
|
||||||
|
# Copy uv (fast Python package manager)
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:0.8.21 /uv /uvx /bin/
|
||||||
|
|
||||||
RUN apk add curl
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
COPY pyproject.toml uv.lock ./
|
||||||
|
|
||||||
COPY requirements.txt /app
|
# Install dependencies into a virtual environment
|
||||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
python3 -m pip install -r requirements.txt
|
uv sync --locked
|
||||||
|
|
||||||
COPY . /app
|
# Copy only app source files
|
||||||
|
COPY blueprints blueprints
|
||||||
|
COPY main.py server.py curl.py tools.py mail.py ./
|
||||||
|
COPY templates templates
|
||||||
|
COPY data data
|
||||||
|
COPY pwa pwa
|
||||||
|
COPY .well-known .well-known
|
||||||
|
|
||||||
# Add mount point for data volume
|
# Clean up caches and pycache
|
||||||
# VOLUME /data
|
RUN rm -rf /root/.cache/uv
|
||||||
|
RUN find . -type d -name "__pycache__" -exec rm -rf {} +
|
||||||
|
|
||||||
ENTRYPOINT ["python3"]
|
|
||||||
CMD ["main.py"]
|
|
||||||
|
|
||||||
FROM builder AS dev-envs
|
### Runtime stage ###
|
||||||
|
FROM python:3.13-alpine AS runtime
|
||||||
|
ENV PATH="/app/.venv/bin:$PATH"
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN addgroup -g 1001 appgroup && \
|
||||||
|
adduser -D -u 1001 -G appgroup -h /app appuser
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
RUN apk add --no-cache curl
|
||||||
|
|
||||||
|
|
||||||
|
# Copy only what’s needed for runtime
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/.venv /app/.venv
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/blueprints /app/blueprints
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/templates /app/templates
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/data /app/data
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/pwa /app/pwa
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/.well-known /app/.well-known
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/main.py /app/
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/server.py /app/
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/curl.py /app/
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/tools.py /app/
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/mail.py /app/
|
||||||
|
|
||||||
|
USER appuser
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
ENTRYPOINT ["python3", "main.py"]
|
||||||
BIN
NathanWoodburn.bsdesign
Normal file
BIN
NathanWoodburn.bsdesign
Normal file
Binary file not shown.
@@ -3,10 +3,10 @@ import os
|
|||||||
from cloudflare import Cloudflare
|
from cloudflare import Cloudflare
|
||||||
from tools import json_response
|
from tools import json_response
|
||||||
|
|
||||||
acme_bp = Blueprint('acme', __name__)
|
app = Blueprint('acme', __name__)
|
||||||
|
|
||||||
|
|
||||||
@acme_bp.route("/hnsdoh-acme", methods=["POST"])
|
@app.route("/hnsdoh-acme", methods=["POST"])
|
||||||
def post():
|
def post():
|
||||||
# Get the TXT record from the request
|
# Get the TXT record from the request
|
||||||
if not request.is_json or not request.json:
|
if not request.is_json or not request.json:
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import requests
|
|||||||
import re
|
import re
|
||||||
from mail import sendEmail
|
from mail import sendEmail
|
||||||
from tools import getClientIP, getGitCommit, json_response, parse_date, get_tools_data
|
from tools import getClientIP, getGitCommit, json_response, parse_date, get_tools_data
|
||||||
from blueprints.sol import sol_bp
|
from blueprints import sol
|
||||||
from dateutil import parser as date_parser
|
from dateutil import parser as date_parser
|
||||||
from blueprints.spotify import get_spotify_track
|
from blueprints.spotify import get_spotify_track
|
||||||
|
from cache_helper import get_nc_config, get_git_latest_activity
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
HTTP_OK = 200
|
HTTP_OK = 200
|
||||||
@@ -17,21 +18,13 @@ HTTP_NOT_FOUND = 404
|
|||||||
HTTP_UNSUPPORTED_MEDIA = 415
|
HTTP_UNSUPPORTED_MEDIA = 415
|
||||||
HTTP_SERVER_ERROR = 500
|
HTTP_SERVER_ERROR = 500
|
||||||
|
|
||||||
api_bp = Blueprint('api', __name__)
|
app = Blueprint('api', __name__, url_prefix='/api/v1')
|
||||||
# Register solana blueprint
|
# Register solana blueprint
|
||||||
api_bp.register_blueprint(sol_bp)
|
app.register_blueprint(sol.app)
|
||||||
|
|
||||||
# Load configuration
|
|
||||||
NC_CONFIG = requests.get(
|
|
||||||
"https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json"
|
|
||||||
).json()
|
|
||||||
|
|
||||||
if 'time-zone' not in NC_CONFIG:
|
|
||||||
NC_CONFIG['time-zone'] = 10
|
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route("/")
|
@app.route("/", strict_slashes=False)
|
||||||
@api_bp.route("/help")
|
@app.route("/help")
|
||||||
def help():
|
def help():
|
||||||
"""Provide API documentation and help."""
|
"""Provide API documentation and help."""
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -40,7 +33,6 @@ def help():
|
|||||||
"/time": "Get the current time",
|
"/time": "Get the current time",
|
||||||
"/timezone": "Get the current timezone",
|
"/timezone": "Get the current timezone",
|
||||||
"/message": "Get the message from the config",
|
"/message": "Get the message from the config",
|
||||||
"/ip": "Get your IP address",
|
|
||||||
"/project": "Get the current project from git",
|
"/project": "Get the current project from git",
|
||||||
"/version": "Get the current version of the website",
|
"/version": "Get the current version of the website",
|
||||||
"/page_date?url=URL&verbose=BOOL": "Get the last modified date of a webpage (verbose is optional, default false)",
|
"/page_date?url=URL&verbose=BOOL": "Get the last modified date of a webpage (verbose is optional, default false)",
|
||||||
@@ -48,6 +40,8 @@ def help():
|
|||||||
"/playing": "Get the currently playing Spotify track",
|
"/playing": "Get the currently playing Spotify track",
|
||||||
"/status": "Just check if the site is up",
|
"/status": "Just check if the site is up",
|
||||||
"/ping": "Just check if the site is up",
|
"/ping": "Just check if the site is up",
|
||||||
|
"/ip": "Get your IP address",
|
||||||
|
"/headers": "Get your request headers",
|
||||||
"/help": "Get this help message"
|
"/help": "Get this help message"
|
||||||
},
|
},
|
||||||
"base_url": "/api/v1",
|
"base_url": "/api/v1",
|
||||||
@@ -56,54 +50,57 @@ def help():
|
|||||||
"status": HTTP_OK
|
"status": HTTP_OK
|
||||||
})
|
})
|
||||||
|
|
||||||
@api_bp.route("/status")
|
@app.route("/status")
|
||||||
@api_bp.route("/ping")
|
@app.route("/ping")
|
||||||
def status():
|
def status():
|
||||||
return json_response(request, "200 OK", HTTP_OK)
|
return json_response(request, "200 OK", HTTP_OK)
|
||||||
|
|
||||||
@api_bp.route("/version")
|
@app.route("/version")
|
||||||
def version():
|
def version():
|
||||||
"""Get the current version of the website."""
|
"""Get the current version of the website."""
|
||||||
return jsonify({"version": getGitCommit()})
|
return jsonify({"version": getGitCommit()})
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route("/time")
|
@app.route("/time")
|
||||||
def time():
|
def time():
|
||||||
"""Get the current time in the configured timezone."""
|
"""Get the current time in the configured timezone."""
|
||||||
timezone_offset = datetime.timedelta(hours=NC_CONFIG["time-zone"])
|
nc_config = get_nc_config()
|
||||||
|
timezone_offset = datetime.timedelta(hours=nc_config["time-zone"])
|
||||||
timezone = datetime.timezone(offset=timezone_offset)
|
timezone = datetime.timezone(offset=timezone_offset)
|
||||||
current_time = datetime.datetime.now(tz=timezone)
|
current_time = datetime.datetime.now(tz=timezone)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"timestring": current_time.strftime("%A, %B %d, %Y %I:%M %p"),
|
"timestring": current_time.strftime("%A, %B %d, %Y %I:%M %p"),
|
||||||
"timestamp": current_time.timestamp(),
|
"timestamp": current_time.timestamp(),
|
||||||
"timezone": NC_CONFIG["time-zone"],
|
"timezone": nc_config["time-zone"],
|
||||||
"timeISO": current_time.isoformat(),
|
"timeISO": current_time.isoformat(),
|
||||||
"ip": getClientIP(request),
|
"ip": getClientIP(request),
|
||||||
"status": HTTP_OK
|
"status": HTTP_OK
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route("/timezone")
|
@app.route("/timezone")
|
||||||
def timezone():
|
def timezone():
|
||||||
"""Get the current timezone setting."""
|
"""Get the current timezone setting."""
|
||||||
|
nc_config = get_nc_config()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"timezone": NC_CONFIG["time-zone"],
|
"timezone": nc_config["time-zone"],
|
||||||
"ip": getClientIP(request),
|
"ip": getClientIP(request),
|
||||||
"status": HTTP_OK
|
"status": HTTP_OK
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route("/message")
|
@app.route("/message")
|
||||||
def message():
|
def message():
|
||||||
"""Get the message from the configuration."""
|
"""Get the message from the configuration."""
|
||||||
|
nc_config = get_nc_config()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"message": NC_CONFIG["message"],
|
"message": nc_config["message"],
|
||||||
"ip": getClientIP(request),
|
"ip": getClientIP(request),
|
||||||
"status": HTTP_OK
|
"status": HTTP_OK
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route("/ip")
|
@app.route("/ip")
|
||||||
def ip():
|
def ip():
|
||||||
"""Get the client's IP address."""
|
"""Get the client's IP address."""
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -112,7 +109,7 @@ def ip():
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route("/email", methods=["POST"])
|
@app.route("/email", methods=["POST"])
|
||||||
def email_post():
|
def email_post():
|
||||||
"""Send an email via the API (requires API key)."""
|
"""Send an email via the API (requires API key)."""
|
||||||
# Verify json
|
# Verify json
|
||||||
@@ -134,30 +131,19 @@ def email_post():
|
|||||||
return sendEmail(data)
|
return sendEmail(data)
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route("/project")
|
@app.route("/project")
|
||||||
def project():
|
def project():
|
||||||
"""Get information about the current git project."""
|
"""Get information about the current git project."""
|
||||||
|
git = get_git_latest_activity()
|
||||||
|
repo_name = git["repo"]["name"].lower()
|
||||||
|
repo_description = git["repo"]["description"]
|
||||||
|
|
||||||
gitinfo = {
|
gitinfo = {
|
||||||
"website": None,
|
"name": repo_name,
|
||||||
|
"description": repo_description,
|
||||||
|
"url": git["repo"]["html_url"],
|
||||||
|
"website": git["repo"].get("website"),
|
||||||
}
|
}
|
||||||
try:
|
|
||||||
git = requests.get(
|
|
||||||
"https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",
|
|
||||||
headers={"Authorization": os.getenv("git_token")},
|
|
||||||
)
|
|
||||||
git = git.json()
|
|
||||||
git = git[0]
|
|
||||||
repo_name = git["repo"]["name"]
|
|
||||||
repo_name = repo_name.lower()
|
|
||||||
repo_description = git["repo"]["description"]
|
|
||||||
gitinfo["name"] = repo_name
|
|
||||||
gitinfo["description"] = repo_description
|
|
||||||
gitinfo["url"] = git["repo"]["html_url"]
|
|
||||||
if "website" in git["repo"]:
|
|
||||||
gitinfo["website"] = git["repo"]["website"]
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error getting git data: {e}")
|
|
||||||
return json_response(request, "500 Internal Server Error", HTTP_SERVER_ERROR)
|
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"repo_name": repo_name,
|
"repo_name": repo_name,
|
||||||
@@ -167,7 +153,7 @@ def project():
|
|||||||
"status": HTTP_OK
|
"status": HTTP_OK
|
||||||
})
|
})
|
||||||
|
|
||||||
@api_bp.route("/tools")
|
@app.route("/tools")
|
||||||
def tools():
|
def tools():
|
||||||
"""Get a list of tools used by Nathan Woodburn."""
|
"""Get a list of tools used by Nathan Woodburn."""
|
||||||
try:
|
try:
|
||||||
@@ -176,14 +162,9 @@ def tools():
|
|||||||
print(f"Error getting tools data: {e}")
|
print(f"Error getting tools data: {e}")
|
||||||
return json_response(request, "500 Internal Server Error", HTTP_SERVER_ERROR)
|
return json_response(request, "500 Internal Server Error", HTTP_SERVER_ERROR)
|
||||||
|
|
||||||
# Remove demo and move demo_url to demo
|
|
||||||
for tool in tools:
|
|
||||||
if "demo_url" in tool:
|
|
||||||
tool["demo"] = tool.pop("demo_url")
|
|
||||||
|
|
||||||
return json_response(request, {"tools": tools}, HTTP_OK)
|
return json_response(request, {"tools": tools}, HTTP_OK)
|
||||||
|
|
||||||
@api_bp.route("/playing")
|
@app.route("/playing")
|
||||||
def playing():
|
def playing():
|
||||||
"""Get the currently playing Spotify track."""
|
"""Get the currently playing Spotify track."""
|
||||||
track_info = get_spotify_track()
|
track_info = get_spotify_track()
|
||||||
@@ -191,7 +172,31 @@ def playing():
|
|||||||
return json_response(request, track_info, HTTP_OK)
|
return json_response(request, track_info, HTTP_OK)
|
||||||
return json_response(request, {"spotify": track_info}, HTTP_OK)
|
return json_response(request, {"spotify": track_info}, HTTP_OK)
|
||||||
|
|
||||||
@api_bp.route("/page_date")
|
|
||||||
|
@app.route("/headers")
|
||||||
|
def headers():
|
||||||
|
"""Get the request headers."""
|
||||||
|
headers = dict(request.headers)
|
||||||
|
|
||||||
|
# For each header, convert list-like headers to lists
|
||||||
|
toremove = []
|
||||||
|
for key, _ in headers.items():
|
||||||
|
# If header is like X- something
|
||||||
|
if key.startswith("X-"):
|
||||||
|
# Remove from headers
|
||||||
|
toremove.append(key)
|
||||||
|
|
||||||
|
|
||||||
|
for key in toremove:
|
||||||
|
headers.pop(key)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"headers": headers,
|
||||||
|
"ip": getClientIP(request),
|
||||||
|
"status": HTTP_OK
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route("/page_date")
|
||||||
def page_date():
|
def page_date():
|
||||||
url = request.args.get("url")
|
url = request.args.get("url")
|
||||||
if not url:
|
if not url:
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ from flask import Blueprint, render_template, request, jsonify
|
|||||||
import markdown
|
import markdown
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
import re
|
import re
|
||||||
from tools import isCurl, getClientIP, getHandshakeScript
|
from functools import lru_cache
|
||||||
|
from tools import isCLI, getClientIP, getHandshakeScript
|
||||||
|
|
||||||
blog_bp = Blueprint('blog', __name__)
|
app = Blueprint('blog', __name__, url_prefix='/blog')
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=32)
|
||||||
def list_page_files():
|
def list_page_files():
|
||||||
blog_pages = os.listdir("data/blog")
|
blog_pages = os.listdir("data/blog")
|
||||||
# Sort pages by modified time, newest first
|
# Sort pages by modified time, newest first
|
||||||
@@ -21,28 +23,43 @@ def list_page_files():
|
|||||||
return blog_pages
|
return blog_pages
|
||||||
|
|
||||||
|
|
||||||
def render_page(date, handshake_scripts=None):
|
@lru_cache(maxsize=64)
|
||||||
# Convert md to html
|
def get_blog_content(date):
|
||||||
|
"""Get and cache blog content."""
|
||||||
if not os.path.exists(f"data/blog/{date}.md"):
|
if not os.path.exists(f"data/blog/{date}.md"):
|
||||||
return render_template("404.html"), 404
|
return None
|
||||||
|
|
||||||
with open(f"data/blog/{date}.md", "r") as f:
|
with open(f"data/blog/{date}.md", "r") as f:
|
||||||
content = f.read()
|
return f.read()
|
||||||
# Get the title from the file name
|
|
||||||
title = date.removesuffix(".md").replace("_", " ")
|
|
||||||
# Convert the md to html
|
@lru_cache(maxsize=64)
|
||||||
content = markdown.markdown(
|
def render_markdown_to_html(content):
|
||||||
|
"""Convert markdown to HTML with caching."""
|
||||||
|
html = markdown.markdown(
|
||||||
content, extensions=['sane_lists', 'codehilite', 'fenced_code'])
|
content, extensions=['sane_lists', 'codehilite', 'fenced_code'])
|
||||||
# Add target="_blank" to all links
|
# Add target="_blank" to all links
|
||||||
content = content.replace('<a href="', '<a target="_blank" href="')
|
html = html.replace('<a href="', '<a target="_blank" href="')
|
||||||
|
html = html.replace("<h4", "<h4 style='margin-bottom:0px;'")
|
||||||
|
html = fix_numbered_lists(html)
|
||||||
|
return html
|
||||||
|
|
||||||
content = content.replace("<h4", "<h4 style='margin-bottom:0px;'")
|
|
||||||
content = fix_numbered_lists(content)
|
def render_page(date, handshake_scripts=None):
|
||||||
|
# Get cached content
|
||||||
|
content = get_blog_content(date)
|
||||||
|
if content is None:
|
||||||
|
return render_template("404.html"), 404
|
||||||
|
|
||||||
|
# Get the title from the file name
|
||||||
|
title = date.removesuffix(".md").replace("_", " ")
|
||||||
|
# Convert the md to html (cached)
|
||||||
|
html_content = render_markdown_to_html(content)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"blog/template.html",
|
"blog/template.html",
|
||||||
title=title,
|
title=title,
|
||||||
content=content,
|
content=html_content,
|
||||||
handshake_scripts=handshake_scripts,
|
handshake_scripts=handshake_scripts,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -108,9 +125,9 @@ def render_home(handshake_scripts: str | None = None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@blog_bp.route("/")
|
@app.route("/", strict_slashes=False)
|
||||||
def index():
|
def index():
|
||||||
if not isCurl(request):
|
if not isCLI(request):
|
||||||
return render_home(handshake_scripts=getHandshakeScript(request.host))
|
return render_home(handshake_scripts=getHandshakeScript(request.host))
|
||||||
|
|
||||||
# Get a list of pages
|
# Get a list of pages
|
||||||
@@ -129,17 +146,16 @@ def index():
|
|||||||
}), 200
|
}), 200
|
||||||
|
|
||||||
|
|
||||||
@blog_bp.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
def path(path):
|
def path(path):
|
||||||
if not isCurl(request):
|
if not isCLI(request):
|
||||||
return render_page(path, handshake_scripts=getHandshakeScript(request.host))
|
return render_page(path, handshake_scripts=getHandshakeScript(request.host))
|
||||||
|
|
||||||
# Convert md to html
|
# Get cached content
|
||||||
if not os.path.exists(f"data/blog/{path}.md"):
|
content = get_blog_content(path)
|
||||||
|
if content is None:
|
||||||
return render_template("404.html"), 404
|
return render_template("404.html"), 404
|
||||||
|
|
||||||
with open(f"data/blog/{path}.md", "r") as f:
|
|
||||||
content = f.read()
|
|
||||||
# Get the title from the file name
|
# Get the title from the file name
|
||||||
title = path.replace("_", " ")
|
title = path.replace("_", " ")
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -152,13 +168,11 @@ def path(path):
|
|||||||
}), 200
|
}), 200
|
||||||
|
|
||||||
|
|
||||||
@blog_bp.route("/<path:path>.md")
|
@app.route("/<path:path>.md")
|
||||||
def path_md(path):
|
def path_md(path):
|
||||||
if not os.path.exists(f"data/blog/{path}.md"):
|
content = get_blog_content(path)
|
||||||
|
if content is None:
|
||||||
return render_template("404.html"), 404
|
return render_template("404.html"), 404
|
||||||
|
|
||||||
with open(f"data/blog/{path}.md", "r") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# Return the raw markdown file
|
# Return the raw markdown file
|
||||||
return content, 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
return content, 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
from flask import Blueprint, render_template, make_response, request, jsonify
|
from flask import Blueprint, render_template, make_response, request, jsonify
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
from tools import getHandshakeScript
|
from functools import lru_cache
|
||||||
|
from tools import getHandshakeScript, error_response, isCLI
|
||||||
|
from curl import get_header, MAX_WIDTH
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import re
|
||||||
|
|
||||||
# Create blueprint
|
# Create blueprint
|
||||||
now_bp = Blueprint('now', __name__)
|
app = Blueprint('now', __name__, url_prefix='/now')
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=16)
|
||||||
def list_page_files():
|
def list_page_files():
|
||||||
now_pages = os.listdir("templates/now")
|
now_pages = os.listdir("templates/now")
|
||||||
now_pages = [
|
now_pages = [
|
||||||
@@ -16,12 +21,14 @@ def list_page_files():
|
|||||||
return now_pages
|
return now_pages
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=16)
|
||||||
def list_dates():
|
def list_dates():
|
||||||
now_pages = list_page_files()
|
now_pages = list_page_files()
|
||||||
now_dates = [page.split(".")[0] for page in now_pages]
|
now_dates = [page.split(".")[0] for page in now_pages]
|
||||||
return now_dates
|
return now_dates
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=8)
|
||||||
def get_latest_date(formatted=False):
|
def get_latest_date(formatted=False):
|
||||||
if formatted:
|
if formatted:
|
||||||
date = list_dates()[0]
|
date = list_dates()[0]
|
||||||
@@ -44,27 +51,115 @@ def render(date, handshake_scripts=None):
|
|||||||
date = date.removesuffix(".html")
|
date = date.removesuffix(".html")
|
||||||
|
|
||||||
if date not in list_dates():
|
if date not in list_dates():
|
||||||
return render_template("404.html"), 404
|
return error_response(request)
|
||||||
|
|
||||||
date_formatted = datetime.datetime.strptime(date, "%y_%m_%d")
|
date_formatted = datetime.datetime.strptime(date, "%y_%m_%d")
|
||||||
date_formatted = date_formatted.strftime("%A, %B %d, %Y")
|
date_formatted = date_formatted.strftime("%A, %B %d, %Y")
|
||||||
return render_template(f"now/{date}.html", DATE=date_formatted, handshake_scripts=handshake_scripts)
|
return render_template(f"now/{date}.html", DATE=date_formatted, handshake_scripts=handshake_scripts)
|
||||||
|
|
||||||
|
def render_curl(date=None):
|
||||||
|
# If the date is not available, render the latest page
|
||||||
|
if date is None:
|
||||||
|
date = get_latest_date()
|
||||||
|
|
||||||
@now_bp.route("/")
|
# Remove .html if present
|
||||||
|
date = date.removesuffix(".html")
|
||||||
|
|
||||||
|
if date not in list_dates():
|
||||||
|
return error_response(request)
|
||||||
|
|
||||||
|
# Format the date nicely
|
||||||
|
date_formatted = datetime.datetime.strptime(date, "%y_%m_%d")
|
||||||
|
date_formatted = date_formatted.strftime("%A, %B %d, %Y")
|
||||||
|
|
||||||
|
# Load HTML
|
||||||
|
with open(f"templates/now/{date}.html", "r", encoding="utf-8") as f:
|
||||||
|
raw_html = f.read().replace("{{ date }}", date_formatted)
|
||||||
|
soup = BeautifulSoup(raw_html, 'html.parser')
|
||||||
|
|
||||||
|
posts = []
|
||||||
|
|
||||||
|
# Find divs matching your pattern
|
||||||
|
divs = soup.find_all("div", style=re.compile(r"max-width:\s*700px", re.IGNORECASE))
|
||||||
|
if not divs:
|
||||||
|
return error_response(request, message="No content found for CLI rendering.")
|
||||||
|
|
||||||
|
for div in divs:
|
||||||
|
# header could be h1/h2/h3 inside the div
|
||||||
|
header_tag = div.find(["h1", "h2", "h3"]) # type: ignore
|
||||||
|
# content is usually one or more <p> tags inside the div
|
||||||
|
p_tags = div.find_all("p") # type: ignore
|
||||||
|
|
||||||
|
if header_tag and p_tags:
|
||||||
|
header_text = header_tag.get_text(strip=True) # type: ignore
|
||||||
|
content_lines = []
|
||||||
|
|
||||||
|
for p in p_tags:
|
||||||
|
# Extract text
|
||||||
|
text = p.get_text(strip=False)
|
||||||
|
|
||||||
|
# Extract any <a> links in the paragraph
|
||||||
|
links = [a.get("href") for a in p.find_all("a", href=True)] # type: ignore
|
||||||
|
# Set max width for text wrapping
|
||||||
|
|
||||||
|
# Wrap text manually
|
||||||
|
wrapped_lines = []
|
||||||
|
for line in text.splitlines():
|
||||||
|
while len(line) > MAX_WIDTH:
|
||||||
|
# Find last space within max_width
|
||||||
|
split_at = line.rfind(' ', 0, MAX_WIDTH)
|
||||||
|
if split_at == -1:
|
||||||
|
split_at = MAX_WIDTH
|
||||||
|
wrapped_lines.append(line[:split_at].rstrip())
|
||||||
|
line = line[split_at:].lstrip()
|
||||||
|
wrapped_lines.append(line)
|
||||||
|
text = "\n".join(wrapped_lines)
|
||||||
|
|
||||||
|
if links:
|
||||||
|
text += "\nLinks: " + ", ".join(links) # type: ignore
|
||||||
|
|
||||||
|
content_lines.append(text)
|
||||||
|
|
||||||
|
content_text = "\n\n".join(content_lines)
|
||||||
|
posts.append({"header": header_text, "content": content_text})
|
||||||
|
|
||||||
|
# Build final response
|
||||||
|
response = ""
|
||||||
|
for post in posts:
|
||||||
|
response += f"[1m{post['header']}[0m\n\n{post['content']}\n\n"
|
||||||
|
|
||||||
|
return render_template("now.ascii", date=date_formatted, content=response, header=get_header())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/", strict_slashes=False)
|
||||||
def index():
|
def index():
|
||||||
|
if isCLI(request):
|
||||||
|
return render_curl()
|
||||||
return render_latest(handshake_scripts=getHandshakeScript(request.host))
|
return render_latest(handshake_scripts=getHandshakeScript(request.host))
|
||||||
|
|
||||||
|
|
||||||
@now_bp.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
def path(path):
|
def path(path):
|
||||||
|
if isCLI(request):
|
||||||
|
return render_curl(path)
|
||||||
|
|
||||||
return render(path, handshake_scripts=getHandshakeScript(request.host))
|
return render(path, handshake_scripts=getHandshakeScript(request.host))
|
||||||
|
|
||||||
|
|
||||||
@now_bp.route("/old")
|
@app.route("/old", strict_slashes=False)
|
||||||
@now_bp.route("/old/")
|
|
||||||
def old():
|
def old():
|
||||||
now_dates = list_dates()[1:]
|
now_dates = list_dates()[1:]
|
||||||
|
if isCLI(request):
|
||||||
|
response = ""
|
||||||
|
for date in now_dates:
|
||||||
|
link = date
|
||||||
|
date_fmt = datetime.datetime.strptime(date, "%y_%m_%d")
|
||||||
|
date_fmt = date_fmt.strftime("%A, %B %d, %Y")
|
||||||
|
response += f"{date_fmt} - /now/{link}\n"
|
||||||
|
return render_template("now.ascii", date="Old Now Pages", content=response, header=get_header())
|
||||||
|
|
||||||
|
|
||||||
html = '<ul class="list-group">'
|
html = '<ul class="list-group">'
|
||||||
html += f'<a style="text-decoration:none;" href="/now"><li class="list-group-item" style="background-color:#000000;color:#ffffff;">{get_latest_date(True)}</li></a>'
|
html += f'<a style="text-decoration:none;" href="/now"><li class="list-group-item" style="background-color:#000000;color:#ffffff;">{get_latest_date(True)}</li></a>'
|
||||||
|
|
||||||
@@ -80,9 +175,9 @@ def old():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@now_bp.route("/now.rss")
|
@app.route("/now.rss")
|
||||||
@now_bp.route("/now.xml")
|
@app.route("/now.xml")
|
||||||
@now_bp.route("/rss.xml")
|
@app.route("/rss.xml")
|
||||||
def rss():
|
def rss():
|
||||||
host = "https://" + request.host
|
host = "https://" + request.host
|
||||||
if ":" in request.host:
|
if ":" in request.host:
|
||||||
@@ -99,7 +194,7 @@ def rss():
|
|||||||
return make_response(rss, 200, {"Content-Type": "application/rss+xml"})
|
return make_response(rss, 200, {"Content-Type": "application/rss+xml"})
|
||||||
|
|
||||||
|
|
||||||
@now_bp.route("/now.json")
|
@app.route("/now.json")
|
||||||
def json():
|
def json():
|
||||||
now_pages = list_page_files()
|
now_pages = list_page_files()
|
||||||
host = "https://" + request.host
|
host = "https://" + request.host
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ from flask import Blueprint, make_response, request
|
|||||||
from tools import error_response
|
from tools import error_response
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
podcast_bp = Blueprint('podcast', __name__)
|
app = Blueprint('podcast', __name__)
|
||||||
|
|
||||||
@podcast_bp.route("/ID1")
|
@app.route("/ID1")
|
||||||
def index():
|
def index():
|
||||||
# Proxy to ID1 url
|
# Proxy to ID1 url
|
||||||
req = requests.get("https://podcasts.c.woodburn.au/ID1")
|
req = requests.get("https://podcasts.c.woodburn.au/ID1")
|
||||||
@@ -16,7 +16,7 @@ def index():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@podcast_bp.route("/ID1/")
|
@app.route("/ID1/")
|
||||||
def contents():
|
def contents():
|
||||||
# Proxy to ID1 url
|
# Proxy to ID1 url
|
||||||
req = requests.get("https://podcasts.c.woodburn.au/ID1/")
|
req = requests.get("https://podcasts.c.woodburn.au/ID1/")
|
||||||
@@ -27,7 +27,7 @@ def contents():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@podcast_bp.route("/ID1/<path:path>")
|
@app.route("/ID1/<path:path>")
|
||||||
def path(path):
|
def path(path):
|
||||||
# Proxy to ID1 url
|
# Proxy to ID1 url
|
||||||
req = requests.get("https://podcasts.c.woodburn.au/ID1/" + path)
|
req = requests.get("https://podcasts.c.woodburn.au/ID1/" + path)
|
||||||
@@ -38,7 +38,7 @@ def path(path):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@podcast_bp.route("/ID1.xml")
|
@app.route("/ID1.xml")
|
||||||
def xml():
|
def xml():
|
||||||
# Proxy to ID1 url
|
# Proxy to ID1 url
|
||||||
req = requests.get("https://podcasts.c.woodburn.au/ID1.xml")
|
req = requests.get("https://podcasts.c.woodburn.au/ID1.xml")
|
||||||
@@ -49,7 +49,7 @@ def xml():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@podcast_bp.route("/podsync.opml")
|
@app.route("/podsync.opml")
|
||||||
def podsync():
|
def podsync():
|
||||||
req = requests.get("https://podcasts.c.woodburn.au/podsync.opml")
|
req = requests.get("https://podcasts.c.woodburn.au/podsync.opml")
|
||||||
if req.status_code != 200:
|
if req.status_code != 200:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import binascii
|
|||||||
import base64
|
import base64
|
||||||
import os
|
import os
|
||||||
|
|
||||||
sol_bp = Blueprint('sol', __name__)
|
app = Blueprint('sol', __name__)
|
||||||
|
|
||||||
SOLANA_HEADERS = {
|
SOLANA_HEADERS = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -55,7 +55,7 @@ def get_solana_address() -> str:
|
|||||||
raise ValueError("SOLANA_ADDRESS is not set. Please ensure the .well-known/wallets/SOL file exists and contains a valid address.")
|
raise ValueError("SOLANA_ADDRESS is not set. Please ensure the .well-known/wallets/SOL file exists and contains a valid address.")
|
||||||
return str(SOLANA_ADDRESS)
|
return str(SOLANA_ADDRESS)
|
||||||
|
|
||||||
@sol_bp.route("/donate", methods=["GET", "OPTIONS"])
|
@app.route("/donate", methods=["GET", "OPTIONS"])
|
||||||
def sol_donate():
|
def sol_donate():
|
||||||
data = {
|
data = {
|
||||||
"icon": "https://nathan.woodburn.au/assets/img/profile.png",
|
"icon": "https://nathan.woodburn.au/assets/img/profile.png",
|
||||||
@@ -90,7 +90,7 @@ def sol_donate():
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@sol_bp.route("/donate/<amount>")
|
@app.route("/donate/<amount>")
|
||||||
def sol_donate_amount(amount):
|
def sol_donate_amount(amount):
|
||||||
data = {
|
data = {
|
||||||
"icon": "https://nathan.woodburn.au/assets/img/profile.png",
|
"icon": "https://nathan.woodburn.au/assets/img/profile.png",
|
||||||
@@ -101,7 +101,7 @@ def sol_donate_amount(amount):
|
|||||||
return jsonify(data), 200, SOLANA_HEADERS
|
return jsonify(data), 200, SOLANA_HEADERS
|
||||||
|
|
||||||
|
|
||||||
@sol_bp.route("/donate/<amount>", methods=["POST"])
|
@app.route("/donate/<amount>", methods=["POST"])
|
||||||
def sol_donate_post(amount):
|
def sol_donate_post(amount):
|
||||||
|
|
||||||
if not request.json:
|
if not request.json:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import requests
|
|||||||
import time
|
import time
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
spotify_bp = Blueprint('spotify', __name__)
|
app = Blueprint('spotify', __name__, url_prefix='/spotify')
|
||||||
|
|
||||||
CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID")
|
CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID")
|
||||||
CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET")
|
CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET")
|
||||||
@@ -25,6 +25,10 @@ def refresh_access_token():
|
|||||||
"""Refresh Spotify access token when expired."""
|
"""Refresh Spotify access token when expired."""
|
||||||
global ACCESS_TOKEN, TOKEN_EXPIRES
|
global ACCESS_TOKEN, TOKEN_EXPIRES
|
||||||
|
|
||||||
|
# If no refresh token, cannot proceed
|
||||||
|
if not REFRESH_TOKEN:
|
||||||
|
return None
|
||||||
|
|
||||||
# If still valid, reuse it
|
# If still valid, reuse it
|
||||||
if ACCESS_TOKEN and time.time() < TOKEN_EXPIRES - 60:
|
if ACCESS_TOKEN and time.time() < TOKEN_EXPIRES - 60:
|
||||||
return ACCESS_TOKEN
|
return ACCESS_TOKEN
|
||||||
@@ -48,7 +52,7 @@ def refresh_access_token():
|
|||||||
TOKEN_EXPIRES = time.time() + token_info.get("expires_in", 3600)
|
TOKEN_EXPIRES = time.time() + token_info.get("expires_in", 3600)
|
||||||
return ACCESS_TOKEN
|
return ACCESS_TOKEN
|
||||||
|
|
||||||
@spotify_bp.route("/login")
|
@app.route("/login")
|
||||||
def login():
|
def login():
|
||||||
auth_query = (
|
auth_query = (
|
||||||
f"{SPOTIFY_AUTH_URL}?response_type=code&client_id={CLIENT_ID}"
|
f"{SPOTIFY_AUTH_URL}?response_type=code&client_id={CLIENT_ID}"
|
||||||
@@ -56,7 +60,7 @@ def login():
|
|||||||
)
|
)
|
||||||
return redirect(auth_query)
|
return redirect(auth_query)
|
||||||
|
|
||||||
@spotify_bp.route("/callback")
|
@app.route("/callback")
|
||||||
def callback():
|
def callback():
|
||||||
code = request.args.get("code")
|
code = request.args.get("code")
|
||||||
if not code:
|
if not code:
|
||||||
@@ -89,41 +93,18 @@ def callback():
|
|||||||
print("Refresh Token:", REFRESH_TOKEN)
|
print("Refresh Token:", REFRESH_TOKEN)
|
||||||
return redirect(url_for("spotify.currently_playing"))
|
return redirect(url_for("spotify.currently_playing"))
|
||||||
|
|
||||||
@spotify_bp.route("/")
|
@app.route("/", strict_slashes=False)
|
||||||
@spotify_bp.route("/currently-playing")
|
@app.route("/playing")
|
||||||
def currently_playing():
|
def currently_playing():
|
||||||
"""Public endpoint showing your current track."""
|
"""Public endpoint showing your current track."""
|
||||||
token = refresh_access_token()
|
track = get_spotify_track()
|
||||||
if not token:
|
|
||||||
return json_response(request, {"error": "Failed to refresh access token"}, 500)
|
|
||||||
|
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
|
||||||
response = requests.get(SPOTIFY_CURRENTLY_PLAYING_URL, headers=headers)
|
|
||||||
|
|
||||||
if response.status_code == 204:
|
|
||||||
return json_response(request, {"message": "Nothing is currently playing."}, 200)
|
|
||||||
elif response.status_code != 200:
|
|
||||||
return json_response(request, {"error": "Spotify API error", "status": response.status_code}, response.status_code)
|
|
||||||
|
|
||||||
data = response.json()
|
|
||||||
if not data.get("item"):
|
|
||||||
return json_response(request, {"message": "Nothing is currently playing."}, 200)
|
|
||||||
|
|
||||||
|
|
||||||
track = {
|
|
||||||
"song_name": data["item"]["name"],
|
|
||||||
"artist": ", ".join([artist["name"] for artist in data["item"]["artists"]]),
|
|
||||||
"album_name": data["item"]["album"]["name"],
|
|
||||||
"album_art": data["item"]["album"]["images"][0]["url"],
|
|
||||||
"is_playing": data["is_playing"]
|
|
||||||
}
|
|
||||||
return json_response(request, {"spotify":track}, 200)
|
return json_response(request, {"spotify":track}, 200)
|
||||||
|
|
||||||
def get_spotify_track():
|
def get_spotify_track():
|
||||||
"""Internal function to get current playing track without HTTP context."""
|
"""Internal function to get current playing track without HTTP context."""
|
||||||
token = refresh_access_token()
|
token = refresh_access_token()
|
||||||
if not token:
|
if not token:
|
||||||
return json_response(request, {"error": "Failed to refresh access token"}, 500)
|
return {"error": "Failed to refresh access token"}
|
||||||
|
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
response = requests.get(SPOTIFY_CURRENTLY_PLAYING_URL, headers=headers)
|
response = requests.get(SPOTIFY_CURRENTLY_PLAYING_URL, headers=headers)
|
||||||
@@ -137,12 +118,13 @@ def get_spotify_track():
|
|||||||
if not data.get("item"):
|
if not data.get("item"):
|
||||||
return {"error": "Nothing is currently playing."}
|
return {"error": "Nothing is currently playing."}
|
||||||
|
|
||||||
|
|
||||||
track = {
|
track = {
|
||||||
"song_name": data["item"]["name"],
|
"song_name": data["item"]["name"],
|
||||||
"artist": ", ".join([artist["name"] for artist in data["item"]["artists"]]),
|
"artist": ", ".join([artist["name"] for artist in data["item"]["artists"]]),
|
||||||
"album_name": data["item"]["album"]["name"],
|
"album_name": data["item"]["album"]["name"],
|
||||||
"album_art": data["item"]["album"]["images"][0]["url"],
|
"album_art": data["item"]["album"]["images"][0]["url"],
|
||||||
"is_playing": data["is_playing"]
|
"is_playing": data["is_playing"],
|
||||||
|
"progress_ms": data.get("progress_ms",0),
|
||||||
|
"duration_ms": data["item"].get("duration_ms",1)
|
||||||
}
|
}
|
||||||
return track
|
return track
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
from flask import Blueprint, request
|
from flask import Blueprint, request
|
||||||
from tools import json_response
|
from tools import json_response
|
||||||
|
|
||||||
template_bp = Blueprint('template', __name__)
|
app = Blueprint('template', __name__)
|
||||||
|
|
||||||
|
|
||||||
@template_bp.route("/")
|
@app.route("/", strict_slashes=False)
|
||||||
def index():
|
def index():
|
||||||
return json_response(request, "Success", 200)
|
return json_response(request, "Success", 200)
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
from flask import Blueprint, render_template, make_response, request, jsonify, send_from_directory, redirect
|
from flask import Blueprint, make_response, request, jsonify, send_from_directory, redirect
|
||||||
|
from tools import error_response
|
||||||
import os
|
import os
|
||||||
|
|
||||||
wk_bp = Blueprint('well-known', __name__)
|
app = Blueprint('well-known', __name__, url_prefix='/.well-known')
|
||||||
|
|
||||||
|
|
||||||
@wk_bp.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
def index(path):
|
def index(path):
|
||||||
return send_from_directory(".well-known", path)
|
return send_from_directory(".well-known", path)
|
||||||
|
|
||||||
|
|
||||||
@wk_bp.route("/wallets/<path:path>")
|
@app.route("/wallets/<path:path>")
|
||||||
def wallets(path):
|
def wallets(path):
|
||||||
if path[0] == "." and 'proof' not in path:
|
if path[0] == "." and 'proof' not in path:
|
||||||
return send_from_directory(
|
return send_from_directory(
|
||||||
@@ -25,10 +26,10 @@ def wallets(path):
|
|||||||
if os.path.isfile(".well-known/wallets/" + path.upper()):
|
if os.path.isfile(".well-known/wallets/" + path.upper()):
|
||||||
return redirect("/.well-known/wallets/" + path.upper(), code=302)
|
return redirect("/.well-known/wallets/" + path.upper(), code=302)
|
||||||
|
|
||||||
return render_template("404.html"), 404
|
return error_response(request)
|
||||||
|
|
||||||
|
|
||||||
@wk_bp.route("/nostr.json")
|
@app.route("/nostr.json")
|
||||||
def nostr():
|
def nostr():
|
||||||
# Get name parameter
|
# Get name parameter
|
||||||
name = request.args.get("name")
|
name = request.args.get("name")
|
||||||
@@ -50,7 +51,7 @@ def nostr():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@wk_bp.route("/xrp-ledger.toml")
|
@app.route("/xrp-ledger.toml")
|
||||||
def xrp():
|
def xrp():
|
||||||
# Create a response with the xrp-ledger.toml file
|
# Create a response with the xrp-ledger.toml file
|
||||||
with open(".well-known/xrp-ledger.toml") as file:
|
with open(".well-known/xrp-ledger.toml") as file:
|
||||||
|
|||||||
250
cache_helper.py
Normal file
250
cache_helper.py
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
"""
|
||||||
|
Cache helper module for expensive API calls and configuration.
|
||||||
|
Provides centralized caching with TTL for external API calls.
|
||||||
|
"""
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
|
||||||
|
# Cache storage for NC_CONFIG with timestamp
|
||||||
|
_nc_config_cache = {"data": None, "timestamp": 0}
|
||||||
|
_nc_config_ttl = 3600 # 1 hour cache
|
||||||
|
|
||||||
|
|
||||||
|
def get_nc_config():
|
||||||
|
"""
|
||||||
|
Get NC_CONFIG with caching (1 hour TTL).
|
||||||
|
Falls back to default config on error.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Configuration dictionary
|
||||||
|
"""
|
||||||
|
global _nc_config_cache
|
||||||
|
current_time = datetime.datetime.now().timestamp()
|
||||||
|
|
||||||
|
# Check if cache is valid
|
||||||
|
if _nc_config_cache["data"] and (current_time - _nc_config_cache["timestamp"]) < _nc_config_ttl:
|
||||||
|
return _nc_config_cache["data"]
|
||||||
|
|
||||||
|
# Fetch new config
|
||||||
|
try:
|
||||||
|
config = requests.get(
|
||||||
|
"https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json",
|
||||||
|
timeout=5
|
||||||
|
).json()
|
||||||
|
_nc_config_cache = {"data": config, "timestamp": current_time}
|
||||||
|
return config
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching NC_CONFIG: {e}")
|
||||||
|
# Return cached data if available, otherwise default
|
||||||
|
if _nc_config_cache["data"]:
|
||||||
|
return _nc_config_cache["data"]
|
||||||
|
return {"time-zone": 10, "message": ""}
|
||||||
|
|
||||||
|
|
||||||
|
# Cache storage for git data
|
||||||
|
_git_data_cache = {"data": None, "timestamp": 0}
|
||||||
|
_git_data_ttl = 300 # 5 minutes cache
|
||||||
|
|
||||||
|
|
||||||
|
def get_git_latest_activity():
|
||||||
|
"""
|
||||||
|
Get latest git activity with caching (5 minute TTL).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Git activity data or default values
|
||||||
|
"""
|
||||||
|
global _git_data_cache
|
||||||
|
current_time = datetime.datetime.now().timestamp()
|
||||||
|
|
||||||
|
# Check if cache is valid
|
||||||
|
if _git_data_cache["data"] and (current_time - _git_data_cache["timestamp"]) < _git_data_ttl:
|
||||||
|
return _git_data_cache["data"]
|
||||||
|
|
||||||
|
# Fetch new data
|
||||||
|
try:
|
||||||
|
git = requests.get(
|
||||||
|
"https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",
|
||||||
|
headers={"Authorization": os.getenv("GIT_AUTH") or os.getenv("git_token") or ""},
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
git_data = git.json()
|
||||||
|
if git_data and len(git_data) > 0:
|
||||||
|
result = git_data[0]
|
||||||
|
_git_data_cache = {"data": result, "timestamp": current_time}
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching git data: {e}")
|
||||||
|
|
||||||
|
# Return cached or default
|
||||||
|
if _git_data_cache["data"]:
|
||||||
|
return _git_data_cache["data"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"repo": {
|
||||||
|
"html_url": "https://nathan.woodburn.au",
|
||||||
|
"name": "nathanwoodburn.github.io",
|
||||||
|
"description": "Personal website",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Cache storage for projects
|
||||||
|
_projects_cache = {"data": None, "timestamp": 0}
|
||||||
|
_projects_ttl = 7200 # 2 hours cache
|
||||||
|
|
||||||
|
|
||||||
|
def get_projects(limit=3):
|
||||||
|
"""
|
||||||
|
Get projects list with caching (2 hour TTL).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
limit (int): Number of projects to return
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of project dictionaries
|
||||||
|
"""
|
||||||
|
global _projects_cache
|
||||||
|
current_time = datetime.datetime.now().timestamp()
|
||||||
|
|
||||||
|
# Check if cache is valid
|
||||||
|
if _projects_cache["data"] and (current_time - _projects_cache["timestamp"]) < _projects_ttl:
|
||||||
|
return _projects_cache["data"][:limit]
|
||||||
|
|
||||||
|
# Fetch new data
|
||||||
|
try:
|
||||||
|
projects = []
|
||||||
|
projectsreq = requests.get(
|
||||||
|
"https://git.woodburn.au/api/v1/users/nathanwoodburn/repos",
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
projects = projectsreq.json()
|
||||||
|
|
||||||
|
# Check for pagination
|
||||||
|
pageNum = 2
|
||||||
|
while 'rel="next"' in projectsreq.headers.get("link", ""):
|
||||||
|
projectsreq = requests.get(
|
||||||
|
f"https://git.woodburn.au/api/v1/users/nathanwoodburn/repos?page={pageNum}",
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
projects += projectsreq.json()
|
||||||
|
pageNum += 1
|
||||||
|
# Safety limit
|
||||||
|
if pageNum > 10:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Process projects
|
||||||
|
for project in projects:
|
||||||
|
if project.get("avatar_url") in ("https://git.woodburn.au/", ""):
|
||||||
|
project["avatar_url"] = "/favicon.png"
|
||||||
|
project["name"] = project["name"].replace("_", " ").replace("-", " ")
|
||||||
|
|
||||||
|
# Sort by last updated
|
||||||
|
projects_sorted = sorted(projects, key=lambda x: x.get("updated_at", ""), reverse=True)
|
||||||
|
|
||||||
|
# Remove duplicates by name
|
||||||
|
seen_names = set()
|
||||||
|
unique_projects = []
|
||||||
|
for project in projects_sorted:
|
||||||
|
if project["name"] not in seen_names:
|
||||||
|
unique_projects.append(project)
|
||||||
|
seen_names.add(project["name"])
|
||||||
|
|
||||||
|
_projects_cache = {"data": unique_projects, "timestamp": current_time}
|
||||||
|
return unique_projects[:limit]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching projects: {e}")
|
||||||
|
if _projects_cache["data"]:
|
||||||
|
return _projects_cache["data"][:limit]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
# Cache storage for uptime status
|
||||||
|
_uptime_cache = {"data": None, "timestamp": 0}
|
||||||
|
_uptime_ttl = 300 # 5 minutes cache
|
||||||
|
|
||||||
|
|
||||||
|
def get_uptime_status():
|
||||||
|
"""
|
||||||
|
Get uptime status with caching (5 minute TTL).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if services are up, False otherwise
|
||||||
|
"""
|
||||||
|
global _uptime_cache
|
||||||
|
current_time = datetime.datetime.now().timestamp()
|
||||||
|
|
||||||
|
# Check if cache is valid
|
||||||
|
if _uptime_cache["data"] is not None and (current_time - _uptime_cache["timestamp"]) < _uptime_ttl:
|
||||||
|
return _uptime_cache["data"]
|
||||||
|
|
||||||
|
# Fetch new data
|
||||||
|
try:
|
||||||
|
uptime = requests.get(
|
||||||
|
"https://uptime.woodburn.au/api/status-page/main/badge",
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
content = uptime.content.decode("utf-8").lower()
|
||||||
|
status = "maintenance" in content or uptime.content.count(b"Up") > 1
|
||||||
|
_uptime_cache = {"data": status, "timestamp": current_time}
|
||||||
|
return status
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching uptime: {e}")
|
||||||
|
# Return cached or default (assume up)
|
||||||
|
if _uptime_cache["data"] is not None:
|
||||||
|
return _uptime_cache["data"]
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# Cached wallet data loaders
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_wallet_tokens():
|
||||||
|
"""
|
||||||
|
Get wallet tokens with caching.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of token dictionaries
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(".well-known/wallets/.tokens") as file:
|
||||||
|
return json.load(file)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading tokens: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_coin_names():
|
||||||
|
"""
|
||||||
|
Get coin names with caching.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary of coin names
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(".well-known/wallets/.coins") as file:
|
||||||
|
return json.load(file)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading coin names: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_wallet_domains():
|
||||||
|
"""
|
||||||
|
Get wallet domains with caching.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary of wallet domains
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if os.path.isfile(".well-known/wallets/.domains"):
|
||||||
|
with open(".well-known/wallets/.domains") as file:
|
||||||
|
return json.load(file)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading domains: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
67
curl.py
67
curl.py
@@ -1,11 +1,13 @@
|
|||||||
from flask import render_template
|
from flask import render_template
|
||||||
from tools import error_response, getAddress, get_tools_data, getClientIP
|
from tools import getAddress, get_tools_data, getClientIP
|
||||||
import os
|
import os
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
import requests
|
|
||||||
from blueprints.spotify import get_spotify_track
|
from blueprints.spotify import get_spotify_track
|
||||||
|
from cache_helper import get_git_latest_activity, get_projects as get_projects_cached
|
||||||
|
|
||||||
|
|
||||||
|
MAX_WIDTH = 80
|
||||||
|
|
||||||
def clean_path(path:str):
|
def clean_path(path:str):
|
||||||
path = path.strip("/ ").lower()
|
path = path.strip("/ ").lower()
|
||||||
# Strip any .html extension
|
# Strip any .html extension
|
||||||
@@ -22,61 +24,26 @@ def get_header():
|
|||||||
with open("templates/header.ascii", "r") as f:
|
with open("templates/header.ascii", "r") as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=16)
|
||||||
def get_current_project():
|
def get_current_project():
|
||||||
git = requests.get(
|
git = get_git_latest_activity()
|
||||||
"https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",
|
repo_name = git["repo"]["name"].lower()
|
||||||
headers={"Authorization": os.getenv("GIT_AUTH") if os.getenv("GIT_AUTH") else os.getenv("git_token")},
|
|
||||||
)
|
|
||||||
git = git.json()
|
|
||||||
git = git[0]
|
|
||||||
repo_name = git["repo"]["name"]
|
|
||||||
repo_name = repo_name.lower()
|
|
||||||
repo_description = git["repo"]["description"]
|
repo_description = git["repo"]["description"]
|
||||||
if not repo_description:
|
if not repo_description:
|
||||||
return f"[1;36m{repo_name}[0m"
|
return f"[1;36m{repo_name}[0m"
|
||||||
return f"[1;36m{repo_name}[0m - [1m{repo_description}[0m"
|
return f"[1;36m{repo_name}[0m - [1m{repo_description}[0m"
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=16)
|
||||||
def get_projects():
|
def get_projects():
|
||||||
projectsreq = requests.get(
|
projects_data = get_projects_cached(limit=5)
|
||||||
"https://git.woodburn.au/api/v1/users/nathanwoodburn/repos"
|
|
||||||
)
|
|
||||||
|
|
||||||
projects = projectsreq.json()
|
|
||||||
|
|
||||||
# Check for next page
|
|
||||||
pageNum = 1
|
|
||||||
while 'rel="next"' in projectsreq.headers["link"]:
|
|
||||||
projectsreq = requests.get(
|
|
||||||
"https://git.woodburn.au/api/v1/users/nathanwoodburn/repos?page="
|
|
||||||
+ str(pageNum)
|
|
||||||
)
|
|
||||||
projects += projectsreq.json()
|
|
||||||
pageNum += 1
|
|
||||||
|
|
||||||
# Sort by last updated
|
|
||||||
projectsList = sorted(
|
|
||||||
projects, key=lambda x: x["updated_at"], reverse=True)
|
|
||||||
projects = ""
|
projects = ""
|
||||||
projectNum = 0
|
for project in projects_data:
|
||||||
includedNames = []
|
projects += f"""[1m{project['name']}[0m - {project['description'] if project['description'] else 'No description'}
|
||||||
while len(includedNames) < 5 and projectNum < len(projectsList):
|
|
||||||
# Avoid duplicates
|
|
||||||
if projectsList[projectNum]["name"] in includedNames:
|
|
||||||
projectNum += 1
|
|
||||||
continue
|
|
||||||
includedNames.append(projectsList[projectNum]["name"])
|
|
||||||
project = projectsList[projectNum]
|
|
||||||
projects += f"""[1m{project['name']}[0m - {project['description'] if project['description'] else 'No description'}
|
|
||||||
{project['html_url']}
|
{project['html_url']}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
projectNum += 1
|
|
||||||
|
|
||||||
return projects
|
return projects
|
||||||
|
|
||||||
def curl_response(request):
|
def curl_response(request):
|
||||||
# Check if <path>.ascii exists
|
# Check if <path>.ascii exists
|
||||||
path = clean_path(request.path)
|
path = clean_path(request.path)
|
||||||
@@ -115,7 +82,6 @@ def curl_response(request):
|
|||||||
tools = get_tools_data()
|
tools = get_tools_data()
|
||||||
return render_template("tools.ascii",header=get_header(),tools=tools), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
return render_template("tools.ascii",header=get_header(),tools=tools), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
|
|
||||||
|
|
||||||
if os.path.exists(f"templates/{path}.ascii"):
|
if os.path.exists(f"templates/{path}.ascii"):
|
||||||
return render_template(f"{path}.ascii",header=get_header()), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
return render_template(f"{path}.ascii",header=get_header()), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
|
|
||||||
@@ -123,4 +89,9 @@ def curl_response(request):
|
|||||||
if os.path.exists(f"templates/{path}.html"):
|
if os.path.exists(f"templates/{path}.html"):
|
||||||
return render_template(f"{path}.html")
|
return render_template(f"{path}.html")
|
||||||
|
|
||||||
return error_response(request)
|
# Return curl error page
|
||||||
|
error = {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested resource was not found on this server."
|
||||||
|
}
|
||||||
|
return render_template("error.ascii",header=get_header(),error=error), 404, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
BIN
data/resume.pdf
BIN
data/resume.pdf
Binary file not shown.
BIN
data/resume_support.pdf
Normal file
BIN
data/resume_support.pdf
Normal file
Binary file not shown.
@@ -10,7 +10,8 @@
|
|||||||
"url": "https://domains.hns.au",
|
"url": "https://domains.hns.au",
|
||||||
"img": "/assets/img/external/HNSAU.webp",
|
"img": "/assets/img/external/HNSAU.webp",
|
||||||
"name": "HNSAU Registry",
|
"name": "HNSAU Registry",
|
||||||
"description": "An easy to use DNS provider and domain reselling platform"
|
"description": "An easy to use DNS provider and domain reselling platform",
|
||||||
|
"enabled": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "https://hns.au",
|
"url": "https://hns.au",
|
||||||
@@ -22,7 +23,8 @@
|
|||||||
"url": "https://hnshosting.au",
|
"url": "https://hnshosting.au",
|
||||||
"img": "/favicon.png",
|
"img": "/favicon.png",
|
||||||
"name": "HNS Hosting",
|
"name": "HNS Hosting",
|
||||||
"description": "Simple Wordpress hosting for Handshake domains with builtin SSL using DANE"
|
"description": "Simple Wordpress hosting for Handshake domains with builtin SSL using DANE",
|
||||||
|
"enabled": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "https://firewallet.au",
|
"url": "https://firewallet.au",
|
||||||
@@ -34,7 +36,8 @@
|
|||||||
"url": "https://shakecities.com",
|
"url": "https://shakecities.com",
|
||||||
"img": "/assets/img/external/HNSW.png",
|
"img": "/assets/img/external/HNSW.png",
|
||||||
"name": "ShakeCities",
|
"name": "ShakeCities",
|
||||||
"description": "A single page website creator where each user's page on their free HNS domain"
|
"description": "A single page website creator where each user's page on their free HNS domain",
|
||||||
|
"enabled": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "https://git.woodburn.au",
|
"url": "https://git.woodburn.au",
|
||||||
@@ -66,5 +69,17 @@
|
|||||||
"img": "https://ipfs.hnsproxy.au/fireportal.png",
|
"img": "https://ipfs.hnsproxy.au/fireportal.png",
|
||||||
"name": "FirePortal",
|
"name": "FirePortal",
|
||||||
"description": "A Handshake domain IPFS gateway that allows you to access IPFS content using Handshake domains"
|
"description": "A Handshake domain IPFS gateway that allows you to access IPFS content using Handshake domains"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://hsd.hns.au/",
|
||||||
|
"img": "/favicon.png",
|
||||||
|
"name": "Fire HSD",
|
||||||
|
"description": "A free public API for Handshake (HSD)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://time.c.woodburn.au/",
|
||||||
|
"img": "/favicon.png",
|
||||||
|
"name": "Timezone Converter",
|
||||||
|
"description": "A simple site and API for converting timezones"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -23,59 +23,60 @@
|
|||||||
"url": "https://code.visualstudio.com/",
|
"url": "https://code.visualstudio.com/",
|
||||||
"description": "Source-code editor developed by Microsoft"
|
"description": "Source-code editor developed by Microsoft"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Vesktop",
|
||||||
|
"type": "Desktop Applications",
|
||||||
|
"url": "https://vesktop.dev/",
|
||||||
|
"description": "Vesktop is a customizable and privacy friendly Discord desktop app!"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Zellij",
|
"name": "Zellij",
|
||||||
"type": "Terminal Tools",
|
"type": "Terminal Tools",
|
||||||
"url": "https://zellij.dev/",
|
"url": "https://zellij.dev/",
|
||||||
"description": "A terminal workspace and multiplexer"
|
"description": "A terminal workspace and multiplexer",
|
||||||
|
"demo": "https://asciinema.c.woodburn.au/a/10"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Fx",
|
"name": "Fx",
|
||||||
"type": "Terminal Tools",
|
"type": "Terminal Tools",
|
||||||
"url": "https://fx.wtf/",
|
"url": "https://fx.wtf/",
|
||||||
"description": "A command-line JSON viewer and processor",
|
"description": "A command-line JSON viewer and processor",
|
||||||
"demo": "<script src=\"https://asciinema.c.woodburn.au/a/4.js\" id=\"asciicast-4\" async=\"true\"></script>",
|
"demo": "https://asciinema.c.woodburn.au/a/4"
|
||||||
"demo_url": "https://asciinema.c.woodburn.au/a/4"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Zoxide",
|
"name": "Zoxide",
|
||||||
"type": "Terminal Tools",
|
"type": "Terminal Tools",
|
||||||
"url": "https://github.com/ajeetdsouza/zoxide",
|
"url": "https://github.com/ajeetdsouza/zoxide",
|
||||||
"description": "cd but with fuzzy matching and other cool features",
|
"description": "cd but with fuzzy matching and other cool features",
|
||||||
"demo": "<script src=\"https://asciinema.c.woodburn.au/a/5.js\" id=\"asciicast-5\" async=\"true\"></script>",
|
"demo": "https://asciinema.c.woodburn.au/a/5"
|
||||||
"demo_url": "https://asciinema.c.woodburn.au/a/5"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Atuin",
|
"name": "Atuin",
|
||||||
"type": "Terminal Tools",
|
"type": "Terminal Tools",
|
||||||
"url": "https://atuin.sh/",
|
"url": "https://atuin.sh/",
|
||||||
"description": "A next-generation shell history manager",
|
"description": "A next-generation shell history manager",
|
||||||
"demo": "<script src=\"https://asciinema.c.woodburn.au/a/6.js\" id=\"asciicast-6\" async=\"true\"></script>",
|
"demo": "https://asciinema.c.woodburn.au/a/6"
|
||||||
"demo_url": "https://asciinema.c.woodburn.au/a/6"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Tmate",
|
"name": "Tmate",
|
||||||
"type": "Terminal Tools",
|
"type": "Terminal Tools",
|
||||||
"url": "https://tmate.io/",
|
"url": "https://tmate.io/",
|
||||||
"description": "Instant terminal sharing",
|
"description": "Instant terminal sharing",
|
||||||
"demo": "<script src=\"https://asciinema.c.woodburn.au/a/7.js\" id=\"asciicast-7\" async=\"true\"></script>",
|
"demo": "https://asciinema.c.woodburn.au/a/7"
|
||||||
"demo_url": "https://asciinema.c.woodburn.au/a/7"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Eza",
|
"name": "Eza",
|
||||||
"type": "Terminal Tools",
|
"type": "Terminal Tools",
|
||||||
"url": "https://eza.rocks/",
|
"url": "https://eza.rocks/",
|
||||||
"description": "A modern replacement for 'ls'",
|
"description": "A modern replacement for 'ls'",
|
||||||
"demo": "<script src=\"https://asciinema.c.woodburn.au/a/8.js\" id=\"asciicast-8\" async=\"true\"></script>",
|
"demo": "https://asciinema.c.woodburn.au/a/8"
|
||||||
"demo_url": "https://asciinema.c.woodburn.au/a/8"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Bat",
|
"name": "Bat",
|
||||||
"type": "Terminal Tools",
|
"type": "Terminal Tools",
|
||||||
"url": "https://github.com/sharkdp/bat",
|
"url": "https://github.com/sharkdp/bat",
|
||||||
"description": "A cat clone with syntax highlighting and Git integration",
|
"description": "A cat clone with syntax highlighting and Git integration",
|
||||||
"demo": "<script src=\"https://asciinema.c.woodburn.au/a/9.js\" id=\"asciicast-9\" async=\"true\"></script>",
|
"demo": "https://asciinema.c.woodburn.au/a/9"
|
||||||
"demo_url": "https://asciinema.c.woodburn.au/a/9"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Oh My Zsh",
|
"name": "Oh My Zsh",
|
||||||
|
|||||||
31
pyproject.toml
Normal file
31
pyproject.toml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
[project]
|
||||||
|
name = "nathanwoodburn-github-io"
|
||||||
|
version = "1.1.0"
|
||||||
|
description = "Nathan.Woodburn Personal Website"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"ansi2html>=1.9.2",
|
||||||
|
"beautifulsoup4>=4.14.2",
|
||||||
|
"cachetools>=6.2.1",
|
||||||
|
"cloudflare>=4.3.1",
|
||||||
|
"flask>=3.1.2",
|
||||||
|
"flask-cors>=6.0.1",
|
||||||
|
"gunicorn>=23.0.0",
|
||||||
|
"markdown>=3.9",
|
||||||
|
"pillow>=12.0.0",
|
||||||
|
"pydantic>=2.12.3",
|
||||||
|
"pygments>=2.19.2",
|
||||||
|
"python-dateutil>=2.9.0.post0",
|
||||||
|
"python-dotenv>=1.2.1",
|
||||||
|
"qrcode>=8.2",
|
||||||
|
"requests>=2.32.5",
|
||||||
|
"solana>=0.36.9",
|
||||||
|
"solders>=0.26.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"pre-commit>=4.4.0",
|
||||||
|
"ruff>=0.14.5",
|
||||||
|
]
|
||||||
493
requirements.txt
493
requirements.txt
@@ -1,18 +1,475 @@
|
|||||||
pydantic
|
# This file was autogenerated by uv via the following command:
|
||||||
flask
|
# uv export --frozen --output-file=requirements.txt
|
||||||
Flask-Cors
|
annotated-types==0.7.0 \
|
||||||
python-dotenv
|
--hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
|
||||||
gunicorn
|
--hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
|
||||||
requests
|
# via pydantic
|
||||||
cloudflare
|
ansi2html==1.9.2 \
|
||||||
qrcode
|
--hash=sha256:3453bf87535d37b827b05245faaa756dbab4ec3d69925e352b6319c3c955c0a5 \
|
||||||
Pillow
|
--hash=sha256:dccb75aa95fb018e5d299be2b45f802952377abfdce0504c17a6ee6ef0a420c5
|
||||||
ansi2html
|
# via nathanwoodburn-github-io
|
||||||
cachetools
|
anyio==4.11.0 \
|
||||||
solana
|
--hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \
|
||||||
solders
|
--hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4
|
||||||
weasyprint
|
# via
|
||||||
markdown
|
# cloudflare
|
||||||
pygments
|
# httpx
|
||||||
beautifulsoup4
|
beautifulsoup4==4.14.2 \
|
||||||
python-dateutil
|
--hash=sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e \
|
||||||
|
--hash=sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515
|
||||||
|
# via nathanwoodburn-github-io
|
||||||
|
blinker==1.9.0 \
|
||||||
|
--hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \
|
||||||
|
--hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc
|
||||||
|
# via flask
|
||||||
|
cachetools==6.2.1 \
|
||||||
|
--hash=sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701 \
|
||||||
|
--hash=sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201
|
||||||
|
# via nathanwoodburn-github-io
|
||||||
|
certifi==2025.10.5 \
|
||||||
|
--hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \
|
||||||
|
--hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43
|
||||||
|
# via
|
||||||
|
# httpcore
|
||||||
|
# httpx
|
||||||
|
# requests
|
||||||
|
cfgv==3.4.0 \
|
||||||
|
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
||||||
|
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
|
||||||
|
# via pre-commit
|
||||||
|
charset-normalizer==3.4.4 \
|
||||||
|
--hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \
|
||||||
|
--hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \
|
||||||
|
--hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \
|
||||||
|
--hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \
|
||||||
|
--hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \
|
||||||
|
--hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \
|
||||||
|
--hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \
|
||||||
|
--hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \
|
||||||
|
--hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \
|
||||||
|
--hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \
|
||||||
|
--hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \
|
||||||
|
--hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \
|
||||||
|
--hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \
|
||||||
|
--hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \
|
||||||
|
--hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \
|
||||||
|
--hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \
|
||||||
|
--hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \
|
||||||
|
--hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \
|
||||||
|
--hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \
|
||||||
|
--hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \
|
||||||
|
--hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \
|
||||||
|
--hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \
|
||||||
|
--hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \
|
||||||
|
--hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \
|
||||||
|
--hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \
|
||||||
|
--hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \
|
||||||
|
--hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \
|
||||||
|
--hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \
|
||||||
|
--hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \
|
||||||
|
--hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \
|
||||||
|
--hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \
|
||||||
|
--hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \
|
||||||
|
--hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \
|
||||||
|
--hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9
|
||||||
|
# via requests
|
||||||
|
click==8.3.0 \
|
||||||
|
--hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \
|
||||||
|
--hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4
|
||||||
|
# via flask
|
||||||
|
cloudflare==4.3.1 \
|
||||||
|
--hash=sha256:6927135a5ee5633d6e2e1952ca0484745e933727aeeb189996d2ad9d292071c6 \
|
||||||
|
--hash=sha256:b1e1c6beeb8d98f63bfe0a1cba874fc4e22e000bcc490544f956c689b3b5b258
|
||||||
|
# via nathanwoodburn-github-io
|
||||||
|
colorama==0.4.6 ; sys_platform == 'win32' \
|
||||||
|
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
|
||||||
|
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
|
||||||
|
# via
|
||||||
|
# click
|
||||||
|
# qrcode
|
||||||
|
construct==2.10.68 \
|
||||||
|
--hash=sha256:7b2a3fd8e5f597a5aa1d614c3bd516fa065db01704c72a1efaaeec6ef23d8b45
|
||||||
|
# via construct-typing
|
||||||
|
construct-typing==0.6.2 \
|
||||||
|
--hash=sha256:948e998cfc003681dc34f2d071c3a688cf35b805cbe107febbc488ef967ccba1 \
|
||||||
|
--hash=sha256:ebea6989ac622d0c4eb457092cef0c7bfbcfa110bd018670fea7064d0bc09e47
|
||||||
|
# via solana
|
||||||
|
distlib==0.4.0 \
|
||||||
|
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||||
|
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||||
|
# via virtualenv
|
||||||
|
distro==1.9.0 \
|
||||||
|
--hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \
|
||||||
|
--hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2
|
||||||
|
# via cloudflare
|
||||||
|
filelock==3.20.0 \
|
||||||
|
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
|
||||||
|
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
|
||||||
|
# via virtualenv
|
||||||
|
flask==3.1.2 \
|
||||||
|
--hash=sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87 \
|
||||||
|
--hash=sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c
|
||||||
|
# via
|
||||||
|
# flask-cors
|
||||||
|
# nathanwoodburn-github-io
|
||||||
|
flask-cors==6.0.1 \
|
||||||
|
--hash=sha256:c7b2cbfb1a31aa0d2e5341eea03a6805349f7a61647daee1a15c46bbe981494c \
|
||||||
|
--hash=sha256:d81bcb31f07b0985be7f48406247e9243aced229b7747219160a0559edd678db
|
||||||
|
# via nathanwoodburn-github-io
|
||||||
|
gunicorn==23.0.0 \
|
||||||
|
--hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \
|
||||||
|
--hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec
|
||||||
|
# via nathanwoodburn-github-io
|
||||||
|
h11==0.16.0 \
|
||||||
|
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
|
||||||
|
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
|
||||||
|
# via httpcore
|
||||||
|
httpcore==1.0.9 \
|
||||||
|
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
|
||||||
|
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
|
||||||
|
# via httpx
|
||||||
|
httpx==0.28.1 \
|
||||||
|
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
|
||||||
|
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
|
||||||
|
# via
|
||||||
|
# cloudflare
|
||||||
|
# solana
|
||||||
|
identify==2.6.15 \
|
||||||
|
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
|
||||||
|
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
|
||||||
|
# via pre-commit
|
||||||
|
idna==3.11 \
|
||||||
|
--hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \
|
||||||
|
--hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902
|
||||||
|
# via
|
||||||
|
# anyio
|
||||||
|
# httpx
|
||||||
|
# requests
|
||||||
|
itsdangerous==2.2.0 \
|
||||||
|
--hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \
|
||||||
|
--hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173
|
||||||
|
# via flask
|
||||||
|
jinja2==3.1.6 \
|
||||||
|
--hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
|
||||||
|
--hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
|
||||||
|
# via flask
|
||||||
|
jsonalias==0.1.1 \
|
||||||
|
--hash=sha256:64f04d935397d579fc94509e1fcb6212f2d081235d9d6395bd10baedf760a769 \
|
||||||
|
--hash=sha256:a56d2888e6397812c606156504e861e8ec00e188005af149f003c787db3d3f18
|
||||||
|
# via solders
|
||||||
|
markdown==3.9 \
|
||||||
|
--hash=sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280 \
|
||||||
|
--hash=sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a
|
||||||
|
# via nathanwoodburn-github-io
|
||||||
|
markupsafe==3.0.3 \
|
||||||
|
--hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \
|
||||||
|
--hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \
|
||||||
|
--hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \
|
||||||
|
--hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \
|
||||||
|
--hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \
|
||||||
|
--hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \
|
||||||
|
--hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \
|
||||||
|
--hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \
|
||||||
|
--hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \
|
||||||
|
--hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \
|
||||||
|
--hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \
|
||||||
|
--hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \
|
||||||
|
--hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \
|
||||||
|
--hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \
|
||||||
|
--hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \
|
||||||
|
--hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \
|
||||||
|
--hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \
|
||||||
|
--hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \
|
||||||
|
--hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \
|
||||||
|
--hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \
|
||||||
|
--hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \
|
||||||
|
--hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \
|
||||||
|
--hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \
|
||||||
|
--hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \
|
||||||
|
--hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \
|
||||||
|
--hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \
|
||||||
|
--hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \
|
||||||
|
--hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \
|
||||||
|
--hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \
|
||||||
|
--hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \
|
||||||
|
--hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \
|
||||||
|
--hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \
|
||||||
|
--hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \
|
||||||
|
--hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \
|
||||||
|
--hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \
|
||||||
|
--hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \
|
||||||
|
--hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \
|
||||||
|
--hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \
|
||||||
|
--hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \
|
||||||
|
--hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \
|
||||||
|
--hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \
|
||||||
|
--hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \
|
||||||
|
--hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \
|
||||||
|
--hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \
|
||||||
|
--hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50
|
||||||
|
# via
|
||||||
|
# flask
|
||||||
|
# jinja2
|
||||||
|
# werkzeug
|
||||||
|
nodeenv==1.9.1 \
|
||||||
|
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||||
|
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
||||||
|
# via pre-commit
|
||||||
|
packaging==25.0 \
|
||||||
|
--hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \
|
||||||
|
--hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f
|
||||||
|
# via gunicorn
|
||||||
|
pillow==12.0.0 \
|
||||||
|
--hash=sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643 \
|
||||||
|
--hash=sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e \
|
||||||
|
--hash=sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6 \
|
||||||
|
--hash=sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b \
|
||||||
|
--hash=sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399 \
|
||||||
|
--hash=sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad \
|
||||||
|
--hash=sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47 \
|
||||||
|
--hash=sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b \
|
||||||
|
--hash=sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52 \
|
||||||
|
--hash=sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d \
|
||||||
|
--hash=sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a \
|
||||||
|
--hash=sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9 \
|
||||||
|
--hash=sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098 \
|
||||||
|
--hash=sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905 \
|
||||||
|
--hash=sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b \
|
||||||
|
--hash=sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01 \
|
||||||
|
--hash=sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca \
|
||||||
|
--hash=sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e \
|
||||||
|
--hash=sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27 \
|
||||||
|
--hash=sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e \
|
||||||
|
--hash=sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8 \
|
||||||
|
--hash=sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a \
|
||||||
|
--hash=sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3 \
|
||||||
|
--hash=sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353 \
|
||||||
|
--hash=sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee \
|
||||||
|
--hash=sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b \
|
||||||
|
--hash=sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a \
|
||||||
|
--hash=sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7 \
|
||||||
|
--hash=sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef \
|
||||||
|
--hash=sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a \
|
||||||
|
--hash=sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07 \
|
||||||
|
--hash=sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4 \
|
||||||
|
--hash=sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4 \
|
||||||
|
--hash=sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe \
|
||||||
|
--hash=sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6 \
|
||||||
|
--hash=sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3 \
|
||||||
|
--hash=sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9 \
|
||||||
|
--hash=sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5 \
|
||||||
|
--hash=sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b \
|
||||||
|
--hash=sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e \
|
||||||
|
--hash=sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab \
|
||||||
|
--hash=sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79 \
|
||||||
|
--hash=sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2 \
|
||||||
|
--hash=sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0 \
|
||||||
|
--hash=sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925 \
|
||||||
|
--hash=sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b \
|
||||||
|
--hash=sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced \
|
||||||
|
--hash=sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c \
|
||||||
|
--hash=sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344 \
|
||||||
|
--hash=sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9 \
|
||||||
|
--hash=sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1
|
||||||
|
# via nathanwoodburn-github-io
|
||||||
|
platformdirs==4.5.0 \
|
||||||
|
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
|
||||||
|
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
|
||||||
|
# via virtualenv
|
||||||
|
pre-commit==4.4.0 \
|
||||||
|
--hash=sha256:b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813 \
|
||||||
|
--hash=sha256:f0233ebab440e9f17cabbb558706eb173d19ace965c68cdce2c081042b4fab15
|
||||||
|
pydantic==2.12.3 \
|
||||||
|
--hash=sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74 \
|
||||||
|
--hash=sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf
|
||||||
|
# via
|
||||||
|
# cloudflare
|
||||||
|
# nathanwoodburn-github-io
|
||||||
|
pydantic-core==2.41.4 \
|
||||||
|
--hash=sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89 \
|
||||||
|
--hash=sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d \
|
||||||
|
--hash=sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2 \
|
||||||
|
--hash=sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af \
|
||||||
|
--hash=sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d \
|
||||||
|
--hash=sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e \
|
||||||
|
--hash=sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894 \
|
||||||
|
--hash=sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa \
|
||||||
|
--hash=sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e \
|
||||||
|
--hash=sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1 \
|
||||||
|
--hash=sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da \
|
||||||
|
--hash=sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025 \
|
||||||
|
--hash=sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5 \
|
||||||
|
--hash=sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d \
|
||||||
|
--hash=sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac \
|
||||||
|
--hash=sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746 \
|
||||||
|
--hash=sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a \
|
||||||
|
--hash=sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84 \
|
||||||
|
--hash=sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12 \
|
||||||
|
--hash=sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2 \
|
||||||
|
--hash=sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e \
|
||||||
|
--hash=sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a \
|
||||||
|
--hash=sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad \
|
||||||
|
--hash=sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4 \
|
||||||
|
--hash=sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf \
|
||||||
|
--hash=sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0 \
|
||||||
|
--hash=sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2 \
|
||||||
|
--hash=sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d \
|
||||||
|
--hash=sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2 \
|
||||||
|
--hash=sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d \
|
||||||
|
--hash=sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02 \
|
||||||
|
--hash=sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616 \
|
||||||
|
--hash=sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced \
|
||||||
|
--hash=sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1 \
|
||||||
|
--hash=sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c \
|
||||||
|
--hash=sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4 \
|
||||||
|
--hash=sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab \
|
||||||
|
--hash=sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564 \
|
||||||
|
--hash=sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554
|
||||||
|
# via pydantic
|
||||||
|
pygments==2.19.2 \
|
||||||
|
--hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \
|
||||||
|
--hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b
|
||||||
|
# via nathanwoodburn-github-io
|
||||||
|
python-dateutil==2.9.0.post0 \
|
||||||
|
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
|
||||||
|
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
|
||||||
|
# via nathanwoodburn-github-io
|
||||||
|
python-dotenv==1.2.1 \
|
||||||
|
--hash=sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6 \
|
||||||
|
--hash=sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61
|
||||||
|
# via nathanwoodburn-github-io
|
||||||
|
pyyaml==6.0.3 \
|
||||||
|
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
||||||
|
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
|
||||||
|
--hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
|
||||||
|
--hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
|
||||||
|
--hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
|
||||||
|
--hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
|
||||||
|
--hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
|
||||||
|
--hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
|
||||||
|
--hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
|
||||||
|
--hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
|
||||||
|
--hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
|
||||||
|
--hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
|
||||||
|
--hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
|
||||||
|
--hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
|
||||||
|
--hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
|
||||||
|
--hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
|
||||||
|
--hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
|
||||||
|
--hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
|
||||||
|
--hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
|
||||||
|
--hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
|
||||||
|
--hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
|
||||||
|
--hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
|
||||||
|
--hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
|
||||||
|
--hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
|
||||||
|
--hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
|
||||||
|
--hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
|
||||||
|
--hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
|
||||||
|
--hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
|
||||||
|
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6
|
||||||
|
# via pre-commit
|
||||||
|
qrcode==8.2 \
|
||||||
|
--hash=sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f \
|
||||||
|
--hash=sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c
|
||||||
|
# via nathanwoodburn-github-io
|
||||||
|
requests==2.32.5 \
|
||||||
|
--hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \
|
||||||
|
--hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf
|
||||||
|
# via nathanwoodburn-github-io
|
||||||
|
ruff==0.14.5 \
|
||||||
|
--hash=sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68 \
|
||||||
|
--hash=sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78 \
|
||||||
|
--hash=sha256:410e781f1122d6be4f446981dd479470af86537fb0b8857f27a6e872f65a38e4 \
|
||||||
|
--hash=sha256:4b700459d4649e2594b31f20a9de33bc7c19976d4746d8d0798ad959621d64a4 \
|
||||||
|
--hash=sha256:6d146132d1ee115f8802356a2dc9a634dbf58184c51bff21f313e8cd1c74899a \
|
||||||
|
--hash=sha256:7497d19dce23976bdaca24345ae131a1d38dcfe1b0850ad8e9e6e4fa321a6e19 \
|
||||||
|
--hash=sha256:88f0770d42b7fa02bbefddde15d235ca3aa24e2f0137388cc15b2dcbb1f7c7a7 \
|
||||||
|
--hash=sha256:8d3b48d7d8aad423d3137af7ab6c8b1e38e4de104800f0d596990f6ada1a9fc1 \
|
||||||
|
--hash=sha256:9d55d7af7166f143c94eae1db3312f9ea8f95a4defef1979ed516dbb38c27621 \
|
||||||
|
--hash=sha256:b595bedf6bc9cab647c4a173a61acf4f1ac5f2b545203ba82f30fcb10b0318fb \
|
||||||
|
--hash=sha256:c01be527ef4c91a6d55e53b337bfe2c0f82af024cc1a33c44792d6844e2331e1 \
|
||||||
|
--hash=sha256:c135d4b681f7401fe0e7312017e41aba9b3160861105726b76cfa14bc25aa367 \
|
||||||
|
--hash=sha256:c83642e6fccfb6dea8b785eb9f456800dcd6a63f362238af5fc0c83d027dd08b \
|
||||||
|
--hash=sha256:d93be8f1fa01022337f1f8f3bcaa7ffee2d0b03f00922c45c2207954f351f465 \
|
||||||
|
--hash=sha256:e2380596653dcd20b057794d55681571a257a42327da8894b93bbd6111aa801f \
|
||||||
|
--hash=sha256:f3b8248123b586de44a8018bcc9fefe31d23dda57a34e6f0e1e53bd51fd63594 \
|
||||||
|
--hash=sha256:f55382725ad0bdb2e8ee2babcbbfb16f124f5a59496a2f6a46f1d9d99d93e6e2 \
|
||||||
|
--hash=sha256:f66e9bb762e68d66e48550b59c74314168ebb46199886c5c5aa0b0fbcc81b151 \
|
||||||
|
--hash=sha256:f7a75236570318c7a30edd7f5491945f0169de738d945ca8784500b517163a72
|
||||||
|
six==1.17.0 \
|
||||||
|
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
|
||||||
|
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
|
||||||
|
# via python-dateutil
|
||||||
|
sniffio==1.3.1 \
|
||||||
|
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
|
||||||
|
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
|
||||||
|
# via
|
||||||
|
# anyio
|
||||||
|
# cloudflare
|
||||||
|
solana==0.36.9 \
|
||||||
|
--hash=sha256:e05824f91f95abe5a687914976e8bc78986386156f2106108c696db998c3c542 \
|
||||||
|
--hash=sha256:f702f6177337c67a982909ef54ef3abce5e795b8cd93edb045bedfa4d13c20c5
|
||||||
|
# via nathanwoodburn-github-io
|
||||||
|
solders==0.26.0 \
|
||||||
|
--hash=sha256:057533892d6fa432c1ce1e2f5e3428802964666c10b57d3d1bcaab86295f046c \
|
||||||
|
--hash=sha256:1b964efbd7c0b38aef3bf4293ea5938517ae649b9a23e7cd147d889931775aab \
|
||||||
|
--hash=sha256:36e6a769c5298b887b7588edb171d93709a89302aef75913fe893d11c653739d \
|
||||||
|
--hash=sha256:3e3973074c17265921c70246a17bcf80972c5b96a3e1ed7f5049101f11865092 \
|
||||||
|
--hash=sha256:5466616610170aab08c627ae01724e425bcf90085bc574da682e9f3bd954900b \
|
||||||
|
--hash=sha256:5946ec3f2a340afa9ce5c2b8ab628ae1dea2ad2235551b1297cafdd7e3e5c51a \
|
||||||
|
--hash=sha256:59b52419452602f697e659199a25acacda8365971c376ef3c0687aecdd929e07 \
|
||||||
|
--hash=sha256:9c1a0ef5daa1a05934af5fb6e7e32eab7c42cede406c80067fee006f461ffc4a \
|
||||||
|
--hash=sha256:b3cc55b971ec6ed1b4466fa7e7e09eee9baba492b8cd9e3204e3e1a0c5a0c4aa
|
||||||
|
# via
|
||||||
|
# nathanwoodburn-github-io
|
||||||
|
# solana
|
||||||
|
soupsieve==2.8 \
|
||||||
|
--hash=sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c \
|
||||||
|
--hash=sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f
|
||||||
|
# via beautifulsoup4
|
||||||
|
typing-extensions==4.15.0 \
|
||||||
|
--hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
|
||||||
|
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
|
||||||
|
# via
|
||||||
|
# beautifulsoup4
|
||||||
|
# cloudflare
|
||||||
|
# construct-typing
|
||||||
|
# pydantic
|
||||||
|
# pydantic-core
|
||||||
|
# solana
|
||||||
|
# solders
|
||||||
|
# typing-inspection
|
||||||
|
typing-inspection==0.4.2 \
|
||||||
|
--hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \
|
||||||
|
--hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464
|
||||||
|
# via pydantic
|
||||||
|
urllib3==2.5.0 \
|
||||||
|
--hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \
|
||||||
|
--hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc
|
||||||
|
# via requests
|
||||||
|
virtualenv==20.35.4 \
|
||||||
|
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
|
||||||
|
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
|
||||||
|
# via pre-commit
|
||||||
|
websockets==15.0.1 \
|
||||||
|
--hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \
|
||||||
|
--hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \
|
||||||
|
--hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \
|
||||||
|
--hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \
|
||||||
|
--hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \
|
||||||
|
--hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \
|
||||||
|
--hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \
|
||||||
|
--hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \
|
||||||
|
--hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \
|
||||||
|
--hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \
|
||||||
|
--hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \
|
||||||
|
--hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \
|
||||||
|
--hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f
|
||||||
|
# via solana
|
||||||
|
werkzeug==3.1.3 \
|
||||||
|
--hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \
|
||||||
|
--hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746
|
||||||
|
# via
|
||||||
|
# flask
|
||||||
|
# flask-cors
|
||||||
|
|||||||
321
server.py
321
server.py
@@ -18,28 +18,38 @@ import qrcode
|
|||||||
from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_H
|
from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_H
|
||||||
from ansi2html import Ansi2HTMLConverter
|
from ansi2html import Ansi2HTMLConverter
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
# Import blueprints
|
# Import blueprints
|
||||||
from blueprints.now import now_bp
|
from blueprints import now, blog, wellknown, api, podcast, acme, spotify
|
||||||
from blueprints.blog import blog_bp
|
from tools import (
|
||||||
from blueprints.wellknown import wk_bp
|
isCLI,
|
||||||
from blueprints.api import api_bp
|
isCrawler,
|
||||||
from blueprints.podcast import podcast_bp
|
getAddress,
|
||||||
from blueprints.acme import acme_bp
|
getFilePath,
|
||||||
from blueprints.spotify import spotify_bp
|
error_response,
|
||||||
from tools import isCurl, isCrawler, getAddress, getFilePath, error_response, getClientIP, json_response, getHandshakeScript, get_tools_data
|
getClientIP,
|
||||||
|
json_response,
|
||||||
|
getHandshakeScript,
|
||||||
|
get_tools_data,
|
||||||
|
)
|
||||||
from curl import curl_response
|
from curl import curl_response
|
||||||
|
from cache_helper import (
|
||||||
|
get_nc_config,
|
||||||
|
get_git_latest_activity,
|
||||||
|
get_projects,
|
||||||
|
get_uptime_status,
|
||||||
|
get_wallet_tokens,
|
||||||
|
get_coin_names,
|
||||||
|
get_wallet_domains,
|
||||||
|
)
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
||||||
# Register blueprints
|
# Register blueprints
|
||||||
app.register_blueprint(now_bp, url_prefix='/now')
|
for module in [now, blog, wellknown, api, podcast, acme, spotify]:
|
||||||
app.register_blueprint(blog_bp, url_prefix='/blog')
|
app.register_blueprint(module.app)
|
||||||
app.register_blueprint(wk_bp, url_prefix='/.well-known')
|
|
||||||
app.register_blueprint(api_bp, url_prefix='/api/v1')
|
|
||||||
app.register_blueprint(podcast_bp)
|
|
||||||
app.register_blueprint(acme_bp)
|
|
||||||
app.register_blueprint(spotify_bp, url_prefix='/spotify')
|
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
@@ -47,34 +57,27 @@ dotenv.load_dotenv()
|
|||||||
|
|
||||||
# Rate limiting for hosting enquiries
|
# Rate limiting for hosting enquiries
|
||||||
EMAIL_REQUEST_COUNT = {} # Track requests by email
|
EMAIL_REQUEST_COUNT = {} # Track requests by email
|
||||||
IP_REQUEST_COUNT = {} # Track requests by IP
|
IP_REQUEST_COUNT = {} # Track requests by IP
|
||||||
EMAIL_RATE_LIMIT = 3 # Max 3 requests per email per hour
|
EMAIL_RATE_LIMIT = 3 # Max 3 requests per email per hour
|
||||||
IP_RATE_LIMIT = 5 # Max 5 requests per IP per hour
|
IP_RATE_LIMIT = 5 # Max 5 requests per IP per hour
|
||||||
RATE_LIMIT_WINDOW = 3600 # 1 hour in seconds
|
RATE_LIMIT_WINDOW = 3600 # 1 hour in seconds
|
||||||
|
|
||||||
RESTRICTED_ROUTES = ["ascii"]
|
RESTRICTED_ROUTES = ["ascii"]
|
||||||
REDIRECT_ROUTES = {
|
REDIRECT_ROUTES = {
|
||||||
"contact": "/#contact"
|
"contact": "/#contact",
|
||||||
}
|
"old": "/now/old",
|
||||||
DOWNLOAD_ROUTES = {
|
"/meet": "https://cloud.woodburn.au/apps/calendar/appointment/PamrmmspWJZr",
|
||||||
"pgp": "data/nathanwoodburn.asc"
|
"/meeting": "https://cloud.woodburn.au/apps/calendar/appointment/PamrmmspWJZr",
|
||||||
|
"/appointment": "https://cloud.woodburn.au/apps/calendar/appointment/PamrmmspWJZr",
|
||||||
}
|
}
|
||||||
|
DOWNLOAD_ROUTES = {"pgp": "data/nathanwoodburn.asc"}
|
||||||
|
|
||||||
SITES = []
|
SITES = []
|
||||||
if os.path.isfile("data/sites.json"):
|
if os.path.isfile("data/sites.json"):
|
||||||
with open("data/sites.json") as file:
|
with open("data/sites.json") as file:
|
||||||
SITES = json.load(file)
|
SITES = json.load(file)
|
||||||
# Remove any sites that are not enabled
|
# Remove any sites that are not enabled
|
||||||
SITES = [
|
SITES = [site for site in SITES if "enabled" not in site or site["enabled"]]
|
||||||
site for site in SITES if "enabled" not in site or site["enabled"]
|
|
||||||
]
|
|
||||||
|
|
||||||
PROJECTS = []
|
|
||||||
PROJECTS_UPDATED = 0
|
|
||||||
|
|
||||||
NC_CONFIG = requests.get(
|
|
||||||
"https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json"
|
|
||||||
).json()
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
@@ -120,6 +123,13 @@ def asset(path):
|
|||||||
return error_response(request)
|
return error_response(request)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/fonts/<path:path>")
|
||||||
|
def fonts(path):
|
||||||
|
if os.path.isfile("templates/assets/fonts/" + path):
|
||||||
|
return send_from_directory("templates/assets/fonts", path)
|
||||||
|
return error_response(request)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/sitemap")
|
@app.route("/sitemap")
|
||||||
@app.route("/sitemap.xml")
|
@app.route("/sitemap.xml")
|
||||||
def sitemap():
|
def sitemap():
|
||||||
@@ -159,6 +169,7 @@ def download(path):
|
|||||||
|
|
||||||
return error_response(request, message="File not found")
|
return error_response(request, message="File not found")
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
# region PWA routes
|
# region PWA routes
|
||||||
|
|
||||||
@@ -183,26 +194,23 @@ def manifest():
|
|||||||
def serviceWorker():
|
def serviceWorker():
|
||||||
return send_from_directory("pwa", "sw.js")
|
return send_from_directory("pwa", "sw.js")
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
# region Misc routes
|
# region Misc routes
|
||||||
|
|
||||||
|
|
||||||
@app.route("/meet")
|
|
||||||
@app.route("/meeting")
|
|
||||||
@app.route("/appointment")
|
|
||||||
def meetingLink():
|
|
||||||
return redirect(
|
|
||||||
"https://cloud.woodburn.au/apps/calendar/appointment/PamrmmspWJZr", code=302
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/links")
|
@app.route("/links")
|
||||||
def links():
|
def links():
|
||||||
return render_template("link.html")
|
return render_template("link.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/actions.json")
|
||||||
|
def sol_actions():
|
||||||
|
return jsonify(
|
||||||
|
{"rules": [{"pathPattern": "/donate**", "apiPath": "/api/v1/donate**"}]}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/<path:function>")
|
@app.route("/api/<path:function>")
|
||||||
def api_legacy(function):
|
def api_legacy(function):
|
||||||
# Check if function is in api blueprint
|
# Check if function is in api blueprint
|
||||||
@@ -213,12 +221,6 @@ def api_legacy(function):
|
|||||||
return error_response(request, message="404 Not Found", code=404)
|
return error_response(request, message="404 Not Found", code=404)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/actions.json")
|
|
||||||
def sol_actions():
|
|
||||||
return jsonify(
|
|
||||||
{"rules": [{"pathPattern": "/donate**", "apiPath": "/api/v1/donate**"}]}
|
|
||||||
)
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region Main routes
|
# region Main routes
|
||||||
@@ -226,9 +228,6 @@ def sol_actions():
|
|||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
global PROJECTS
|
|
||||||
global PROJECTS_UPDATED
|
|
||||||
|
|
||||||
# Check if host if podcast.woodburn.au
|
# Check if host if podcast.woodburn.au
|
||||||
if "podcast.woodburn.au" in request.host:
|
if "podcast.woodburn.au" in request.host:
|
||||||
return render_template("podcast.html")
|
return render_template("podcast.html")
|
||||||
@@ -244,7 +243,7 @@ def index():
|
|||||||
# Always load if load is in the query string
|
# Always load if load is in the query string
|
||||||
if request.args.get("load"):
|
if request.args.get("load"):
|
||||||
loaded = False
|
loaded = False
|
||||||
if isCurl(request):
|
if isCLI(request):
|
||||||
return curl_response(request)
|
return curl_response(request)
|
||||||
|
|
||||||
if not loaded and not isCrawler(request):
|
if not loaded and not isCrawler(request):
|
||||||
@@ -259,79 +258,22 @@ def index():
|
|||||||
resp.set_cookie("loaded", "true", max_age=604800)
|
resp.set_cookie("loaded", "true", max_age=604800)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
try:
|
# Use cached git data
|
||||||
git = requests.get(
|
git = get_git_latest_activity()
|
||||||
"https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",
|
repo_name = git["repo"]["name"].lower()
|
||||||
headers={"Authorization": os.getenv("GIT_AUTH") if os.getenv("GIT_AUTH") else os.getenv("git_token")},
|
repo_description = git["repo"]["description"]
|
||||||
)
|
|
||||||
git = git.json()
|
|
||||||
git = git[0]
|
|
||||||
repo_name = git["repo"]["name"]
|
|
||||||
repo_name = repo_name.lower()
|
|
||||||
repo_description = git["repo"]["description"]
|
|
||||||
except Exception as e:
|
|
||||||
repo_name = "nathanwoodburn.github.io"
|
|
||||||
repo_description = "Personal website"
|
|
||||||
git = {
|
|
||||||
"repo": {
|
|
||||||
"html_url": "https://nathan.woodburn.au",
|
|
||||||
"name": "nathanwoodburn.github.io",
|
|
||||||
"description": "Personal website",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
print(f"Error getting git data: {e}")
|
|
||||||
|
|
||||||
# Get only repo names for the newest updates
|
# Use cached projects data
|
||||||
if PROJECTS == [] or PROJECTS_UPDATED < (datetime.datetime.now() - datetime.timedelta(
|
projects = get_projects(limit=3)
|
||||||
hours=2
|
|
||||||
)).timestamp():
|
|
||||||
projectsreq = requests.get(
|
|
||||||
"https://git.woodburn.au/api/v1/users/nathanwoodburn/repos"
|
|
||||||
)
|
|
||||||
|
|
||||||
PROJECTS = projectsreq.json()
|
|
||||||
|
|
||||||
# Check for next page
|
|
||||||
pageNum = 1
|
|
||||||
while 'rel="next"' in projectsreq.headers["link"]:
|
|
||||||
projectsreq = requests.get(
|
|
||||||
"https://git.woodburn.au/api/v1/users/nathanwoodburn/repos?page="
|
|
||||||
+ str(pageNum)
|
|
||||||
)
|
|
||||||
PROJECTS += projectsreq.json()
|
|
||||||
pageNum += 1
|
|
||||||
|
|
||||||
for project in PROJECTS:
|
|
||||||
if (
|
|
||||||
project["avatar_url"] == "https://git.woodburn.au/"
|
|
||||||
or project["avatar_url"] == ""
|
|
||||||
):
|
|
||||||
project["avatar_url"] = "/favicon.png"
|
|
||||||
project["name"] = project["name"].replace(
|
|
||||||
"_", " ").replace("-", " ")
|
|
||||||
# Sort by last updated
|
|
||||||
projectsList = sorted(
|
|
||||||
PROJECTS, key=lambda x: x["updated_at"], reverse=True)
|
|
||||||
PROJECTS = []
|
|
||||||
projectNames = []
|
|
||||||
projectNum = 0
|
|
||||||
while len(PROJECTS) < 3:
|
|
||||||
if projectsList[projectNum]["name"] not in projectNames:
|
|
||||||
PROJECTS.append(projectsList[projectNum])
|
|
||||||
projectNames.append(projectsList[projectNum]["name"])
|
|
||||||
projectNum += 1
|
|
||||||
PROJECTS_UPDATED = datetime.datetime.now().timestamp()
|
|
||||||
|
|
||||||
|
# Use cached uptime status
|
||||||
|
uptime = get_uptime_status()
|
||||||
custom = ""
|
custom = ""
|
||||||
# Check for downtime
|
|
||||||
uptime = requests.get(
|
|
||||||
"https://uptime.woodburn.au/api/status-page/main/badge")
|
|
||||||
uptime = uptime.content.count(b"Up") > 1
|
|
||||||
|
|
||||||
if uptime:
|
if uptime:
|
||||||
custom += "<style>#downtime{display:none !important;}</style>"
|
custom += "<style>#downtime{display:none !important;}</style>"
|
||||||
else:
|
else:
|
||||||
custom += "<style>#downtime{opacity:1;}</style>"
|
custom += "<style>#downtime{opacity:1;}</style>"
|
||||||
|
|
||||||
# Special names
|
# Special names
|
||||||
if repo_name == "nathanwoodburn.github.io":
|
if repo_name == "nathanwoodburn.github.io":
|
||||||
repo_name = "Nathan.Woodburn/"
|
repo_name = "Nathan.Woodburn/"
|
||||||
@@ -339,8 +281,9 @@ def index():
|
|||||||
html_url = git["repo"]["html_url"]
|
html_url = git["repo"]["html_url"]
|
||||||
repo = '<a href="' + html_url + '" target="_blank">' + repo_name + "</a>"
|
repo = '<a href="' + html_url + '" target="_blank">' + repo_name + "</a>"
|
||||||
|
|
||||||
# Get time
|
# Get time using cached config
|
||||||
timezone_offset = datetime.timedelta(hours=NC_CONFIG["time-zone"])
|
nc_config = get_nc_config()
|
||||||
|
timezone_offset = datetime.timedelta(hours=nc_config["time-zone"])
|
||||||
timezone = datetime.timezone(offset=timezone_offset)
|
timezone = datetime.timezone(offset=timezone_offset)
|
||||||
time = datetime.datetime.now(tz=timezone)
|
time = datetime.datetime.now(tz=timezone)
|
||||||
|
|
||||||
@@ -363,7 +306,7 @@ def index():
|
|||||||
setInterval(updateClock, 1000);
|
setInterval(updateClock, 1000);
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
time += f"startClock({NC_CONFIG['time-zone']});"
|
time += f"startClock({nc_config['time-zone']});"
|
||||||
time += "</script>"
|
time += "</script>"
|
||||||
|
|
||||||
HNSaddress = getAddress("HNS")
|
HNSaddress = getAddress("HNS")
|
||||||
@@ -383,9 +326,9 @@ def index():
|
|||||||
repo_description=repo_description,
|
repo_description=repo_description,
|
||||||
custom=custom,
|
custom=custom,
|
||||||
sites=SITES,
|
sites=SITES,
|
||||||
projects=PROJECTS,
|
projects=projects,
|
||||||
time=time,
|
time=time,
|
||||||
message=NC_CONFIG.get("message",""),
|
message=nc_config.get("message", ""),
|
||||||
),
|
),
|
||||||
200,
|
200,
|
||||||
{"Content-Type": "text/html"},
|
{"Content-Type": "text/html"},
|
||||||
@@ -394,41 +337,34 @@ def index():
|
|||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
# region Donate
|
# region Donate
|
||||||
|
|
||||||
|
|
||||||
@app.route("/donate")
|
@app.route("/donate")
|
||||||
def donate():
|
def donate():
|
||||||
if isCurl(request):
|
if isCLI(request):
|
||||||
return curl_response(request)
|
return curl_response(request)
|
||||||
|
|
||||||
coinList = os.listdir(".well-known/wallets")
|
coinList = os.listdir(".well-known/wallets")
|
||||||
coinList = [file for file in coinList if file[0] != "."]
|
coinList = [file for file in coinList if file[0] != "."]
|
||||||
coinList.sort()
|
coinList.sort()
|
||||||
|
|
||||||
tokenList = []
|
tokenList = get_wallet_tokens()
|
||||||
|
coinNames = get_coin_names()
|
||||||
with open(".well-known/wallets/.tokens") as file:
|
|
||||||
tokenList = file.read()
|
|
||||||
tokenList = json.loads(tokenList)
|
|
||||||
|
|
||||||
coinNames = {}
|
|
||||||
with open(".well-known/wallets/.coins") as file:
|
|
||||||
coinNames = file.read()
|
|
||||||
coinNames = json.loads(coinNames)
|
|
||||||
|
|
||||||
coins = ""
|
coins = ""
|
||||||
default_coins = ["btc", "eth", "hns", "sol", "xrp", "ada", "dot"]
|
default_coins = ["btc", "eth", "hns", "sol", "xrp", "ada", "dot"]
|
||||||
|
|
||||||
for file in coinList:
|
for file in coinList:
|
||||||
if file in coinNames:
|
coin_name = coinNames.get(file, file)
|
||||||
coins += f'<a class="dropdown-item" style="{"display:none;" if file.lower() not in default_coins else ""}" href="?c={file.lower()}">{coinNames[file]}</a>'
|
display_style = "" if file.lower() in default_coins else "display:none;"
|
||||||
else:
|
coins += f'<a class="dropdown-item" style="{display_style}" href="?c={file.lower()}">{coin_name}</a>'
|
||||||
coins += f'<a class="dropdown-item" style="{"display:none;" if file.lower() not in default_coins else ""}" href="?c={file.lower()}">{file}</a>'
|
|
||||||
|
|
||||||
for token in tokenList:
|
for token in tokenList:
|
||||||
if token["chain"] != "null":
|
chain_display = f" on {token['chain']}" if token["chain"] != "null" else ""
|
||||||
coins += f'<a class="dropdown-item" style="display:none;" href="?t={token["symbol"].lower()}&c={token["chain"].lower()}">{token["name"]} ({token["symbol"] + " on " if token["symbol"] != token["name"] else ""}{token["chain"]})</a>'
|
symbol_display = f" ({token['symbol']}{chain_display})" if token["symbol"] != token["name"] else chain_display
|
||||||
else:
|
coins += f'<a class="dropdown-item" style="display:none;" href="?t={token["symbol"].lower()}&c={token["chain"].lower()}">{token["name"]}{symbol_display}</a>'
|
||||||
coins += f'<a class="dropdown-item" style="display:none;" href="?t={token["symbol"].lower()}&c={token["chain"].lower()}">{token["name"]} ({token["symbol"] if token["symbol"] != token["name"] else ""})</a>'
|
|
||||||
|
|
||||||
crypto = request.args.get("c")
|
crypto = request.args.get("c")
|
||||||
if not crypto:
|
if not crypto:
|
||||||
@@ -455,7 +391,6 @@ def donate():
|
|||||||
token = {"name": "Unknown token", "symbol": token, "chain": crypto}
|
token = {"name": "Unknown token", "symbol": token, "chain": crypto}
|
||||||
|
|
||||||
address = ""
|
address = ""
|
||||||
domain = ""
|
|
||||||
cryptoHTML = ""
|
cryptoHTML = ""
|
||||||
|
|
||||||
proof = ""
|
proof = ""
|
||||||
@@ -465,10 +400,12 @@ def donate():
|
|||||||
if os.path.isfile(f".well-known/wallets/{crypto}"):
|
if os.path.isfile(f".well-known/wallets/{crypto}"):
|
||||||
with open(f".well-known/wallets/{crypto}") as file:
|
with open(f".well-known/wallets/{crypto}") as file:
|
||||||
address = file.read()
|
address = file.read()
|
||||||
|
coin_display = coinNames.get(crypto, crypto)
|
||||||
if not token:
|
if not token:
|
||||||
cryptoHTML += f"<br>Donate with {coinNames[crypto] if crypto in coinNames else crypto}:"
|
cryptoHTML += f"<br>Donate with {coin_display}:"
|
||||||
else:
|
else:
|
||||||
cryptoHTML += f'<br>Donate with {token["name"]} {"("+token["symbol"]+") " if token["symbol"] != token["name"] else ""}on {crypto}:'
|
token_symbol = f" ({token['symbol']})" if token['symbol'] != token['name'] else ""
|
||||||
|
cryptoHTML += f"<br>Donate with {token['name']}{token_symbol} on {crypto}:"
|
||||||
cryptoHTML += f'<br><code data-bs-toggle="tooltip" data-bss-tooltip="" id="crypto-address" class="address" style="color: rgb(242,90,5);display: inline-block;" data-bs-original-title="Click to copy">{address}</code>'
|
cryptoHTML += f'<br><code data-bs-toggle="tooltip" data-bss-tooltip="" id="crypto-address" class="address" style="color: rgb(242,90,5);display: inline-block;" data-bs-original-title="Click to copy">{address}</code>'
|
||||||
|
|
||||||
if proof:
|
if proof:
|
||||||
@@ -476,25 +413,23 @@ def donate():
|
|||||||
elif token:
|
elif token:
|
||||||
if "address" in token:
|
if "address" in token:
|
||||||
address = token["address"]
|
address = token["address"]
|
||||||
cryptoHTML += f'<br>Donate with {token["name"]} {"("+token["symbol"]+")" if token["symbol"] != token["name"] else ""}{" on "+crypto if crypto != "NULL" else ""}:'
|
token_symbol = f" ({token['symbol']})" if token['symbol'] != token['name'] else ""
|
||||||
|
chain_display = f" on {crypto}" if crypto != 'NULL' else ""
|
||||||
|
cryptoHTML += f"<br>Donate with {token['name']}{token_symbol}{chain_display}:"
|
||||||
cryptoHTML += f'<br><code data-bs-toggle="tooltip" data-bss-tooltip="" id="crypto-address" class="address" style="color: rgb(242,90,5);display: inline-block;" data-bs-original-title="Click to copy">{address}</code>'
|
cryptoHTML += f'<br><code data-bs-toggle="tooltip" data-bss-tooltip="" id="crypto-address" class="address" style="color: rgb(242,90,5);display: inline-block;" data-bs-original-title="Click to copy">{address}</code>'
|
||||||
if proof:
|
if proof:
|
||||||
cryptoHTML += proof
|
cryptoHTML += proof
|
||||||
else:
|
else:
|
||||||
cryptoHTML += f'<br>Invalid offchain token: {token["symbol"]}<br>'
|
cryptoHTML += f"<br>Invalid offchain token: {token['symbol']}<br>"
|
||||||
else:
|
else:
|
||||||
cryptoHTML += f"<br>Invalid chain: {crypto}<br>"
|
cryptoHTML += f"<br>Invalid chain: {crypto}<br>"
|
||||||
|
|
||||||
if os.path.isfile(".well-known/wallets/.domains"):
|
domains = get_wallet_domains()
|
||||||
# Get json of all domains
|
if crypto in domains:
|
||||||
with open(".well-known/wallets/.domains") as file:
|
domain = domains[crypto]
|
||||||
domains = file.read()
|
cryptoHTML += "<br>Or send to this domain on compatible wallets:<br>"
|
||||||
domains = json.loads(domains)
|
cryptoHTML += f'<code data-bs-toggle="tooltip" data-bss-tooltip="" id="crypto-domain" class="address" style="color: rgb(242,90,5);display: block;" data-bs-original-title="Click to copy">{domain}</code>'
|
||||||
|
|
||||||
if crypto in domains:
|
|
||||||
domain = domains[crypto]
|
|
||||||
cryptoHTML += "<br>Or send to this domain on compatible wallets:<br>"
|
|
||||||
cryptoHTML += f'<code data-bs-toggle="tooltip" data-bss-tooltip="" id="crypto-domain" class="address" style="color: rgb(242,90,5);display: block;" data-bs-original-title="Click to copy">{domain}</code>'
|
|
||||||
if address:
|
if address:
|
||||||
cryptoHTML += (
|
cryptoHTML += (
|
||||||
'<br><img src="/address/'
|
'<br><img src="/address/'
|
||||||
@@ -537,29 +472,33 @@ def qraddress(address):
|
|||||||
@app.route("/qrcode/<path:data>")
|
@app.route("/qrcode/<path:data>")
|
||||||
@app.route("/qr/<path:data>")
|
@app.route("/qr/<path:data>")
|
||||||
def qrcodee(data):
|
def qrcodee(data):
|
||||||
qr = qrcode.QRCode(
|
qr = qrcode.QRCode(error_correction=ERROR_CORRECT_H, box_size=10, border=2)
|
||||||
error_correction=ERROR_CORRECT_H, box_size=10, border=2)
|
|
||||||
qr.add_data(data)
|
qr.add_data(data)
|
||||||
qr.make()
|
qr.make()
|
||||||
|
|
||||||
qr_image: Image.Image = qr.make_image(
|
qr_image: Image.Image = qr.make_image(
|
||||||
fill_color="black", back_color="white").convert('RGB') # type: ignore
|
fill_color="black", back_color="white"
|
||||||
|
).convert("RGB") # type: ignore
|
||||||
|
|
||||||
# Add logo
|
# Add logo
|
||||||
logo = Image.open("templates/assets/img/favicon/logo.png")
|
logo = Image.open("templates/assets/img/favicon/logo.png")
|
||||||
basewidth = qr_image.size[0]//3
|
basewidth = qr_image.size[0] // 3
|
||||||
wpercent = (basewidth / float(logo.size[0]))
|
wpercent = basewidth / float(logo.size[0])
|
||||||
hsize = int((float(logo.size[1]) * float(wpercent)))
|
hsize = int((float(logo.size[1]) * float(wpercent)))
|
||||||
logo = logo.resize((basewidth, hsize), Image.Resampling.LANCZOS)
|
logo = logo.resize((basewidth, hsize), Image.Resampling.LANCZOS)
|
||||||
pos = ((qr_image.size[0] - logo.size[0]) // 2,
|
pos = (
|
||||||
(qr_image.size[1] - logo.size[1]) // 2)
|
(qr_image.size[0] - logo.size[0]) // 2,
|
||||||
|
(qr_image.size[1] - logo.size[1]) // 2,
|
||||||
|
)
|
||||||
qr_image.paste(logo, pos, mask=logo)
|
qr_image.paste(logo, pos, mask=logo)
|
||||||
|
|
||||||
qr_image.save("/tmp/qr_code.png")
|
qr_image.save("/tmp/qr_code.png")
|
||||||
return send_file("/tmp/qr_code.png", mimetype="image/png")
|
return send_file("/tmp/qr_code.png", mimetype="image/png")
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
@app.route("/supersecretpath")
|
@app.route("/supersecretpath")
|
||||||
def supersecretpath():
|
def supersecretpath():
|
||||||
ascii_art = ""
|
ascii_art = ""
|
||||||
@@ -596,15 +535,18 @@ def hosting_post():
|
|||||||
|
|
||||||
# Check email rate limit
|
# Check email rate limit
|
||||||
if email in EMAIL_REQUEST_COUNT:
|
if email in EMAIL_REQUEST_COUNT:
|
||||||
if (current_time - EMAIL_REQUEST_COUNT[email]["last_reset"]) > RATE_LIMIT_WINDOW:
|
if (
|
||||||
|
current_time - EMAIL_REQUEST_COUNT[email]["last_reset"]
|
||||||
|
) > RATE_LIMIT_WINDOW:
|
||||||
# Reset counter if the time window has passed
|
# Reset counter if the time window has passed
|
||||||
EMAIL_REQUEST_COUNT[email] = {
|
EMAIL_REQUEST_COUNT[email] = {"count": 1, "last_reset": current_time}
|
||||||
"count": 1, "last_reset": current_time}
|
|
||||||
else:
|
else:
|
||||||
# Increment counter
|
# Increment counter
|
||||||
EMAIL_REQUEST_COUNT[email]["count"] += 1
|
EMAIL_REQUEST_COUNT[email]["count"] += 1
|
||||||
if EMAIL_REQUEST_COUNT[email]["count"] > EMAIL_RATE_LIMIT:
|
if EMAIL_REQUEST_COUNT[email]["count"] > EMAIL_RATE_LIMIT:
|
||||||
return json_response(request, "Rate limit exceeded. Please try again later.", 429)
|
return json_response(
|
||||||
|
request, "Rate limit exceeded. Please try again later.", 429
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# First request for this email
|
# First request for this email
|
||||||
EMAIL_REQUEST_COUNT[email] = {"count": 1, "last_reset": current_time}
|
EMAIL_REQUEST_COUNT[email] = {"count": 1, "last_reset": current_time}
|
||||||
@@ -618,7 +560,9 @@ def hosting_post():
|
|||||||
# Increment counter
|
# Increment counter
|
||||||
IP_REQUEST_COUNT[ip]["count"] += 1
|
IP_REQUEST_COUNT[ip]["count"] += 1
|
||||||
if IP_REQUEST_COUNT[ip]["count"] > IP_RATE_LIMIT:
|
if IP_REQUEST_COUNT[ip]["count"] > IP_RATE_LIMIT:
|
||||||
return json_response(request, "Rate limit exceeded. Please try again later.", 429)
|
return json_response(
|
||||||
|
request, "Rate limit exceeded. Please try again later.", 429
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# First request for this IP
|
# First request for this IP
|
||||||
IP_REQUEST_COUNT[ip] = {"count": 1, "last_reset": current_time}
|
IP_REQUEST_COUNT[ip] = {"count": 1, "last_reset": current_time}
|
||||||
@@ -678,33 +622,41 @@ def hosting_post():
|
|||||||
return json_response(request, "Enquiry sent", 200)
|
return json_response(request, "Enquiry sent", 200)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/resume")
|
||||||
|
def resume():
|
||||||
|
# Check if arg for support is passed
|
||||||
|
support = request.args.get("support")
|
||||||
|
return render_template("resume.html", support=support)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/resume.pdf")
|
@app.route("/resume.pdf")
|
||||||
def resume_pdf():
|
def resume_pdf():
|
||||||
# Check if file exists
|
# Check if arg for support is passed
|
||||||
if os.path.isfile("data/resume.pdf"):
|
support = request.args.get("support")
|
||||||
return send_file("data/resume.pdf")
|
if support:
|
||||||
return error_response(request, message="Resume not found")
|
return send_file("data/resume_support.pdf")
|
||||||
|
return send_file("data/resume.pdf")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/tools")
|
@app.route("/tools")
|
||||||
def tools():
|
def tools():
|
||||||
if isCurl(request):
|
if isCLI(request):
|
||||||
return curl_response(request)
|
return curl_response(request)
|
||||||
return render_template("tools.html", tools=get_tools_data())
|
return render_template("tools.html", tools=get_tools_data())
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
# region Error Catching
|
# region Error Catching
|
||||||
|
|
||||||
|
|
||||||
# Catch all for GET requests
|
# Catch all for GET requests
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
def catch_all(path: str):
|
def catch_all(path: str):
|
||||||
|
|
||||||
if path.lower().replace(".html", "") in RESTRICTED_ROUTES:
|
if path.lower().replace(".html", "") in RESTRICTED_ROUTES:
|
||||||
return error_response(request, message="Restricted route", code=403)
|
return error_response(request, message="Restricted route", code=403)
|
||||||
|
|
||||||
# If curl request, return curl response
|
# If curl request, return curl response
|
||||||
if isCurl(request):
|
if isCLI(request):
|
||||||
return curl_response(request)
|
return curl_response(request)
|
||||||
|
|
||||||
if path in REDIRECT_ROUTES:
|
if path in REDIRECT_ROUTES:
|
||||||
@@ -712,17 +664,23 @@ def catch_all(path: str):
|
|||||||
|
|
||||||
# If file exists, load it
|
# If file exists, load it
|
||||||
if os.path.isfile("templates/" + path):
|
if os.path.isfile("templates/" + path):
|
||||||
return render_template(path, handshake_scripts=getHandshakeScript(request.host), sites=SITES)
|
return render_template(
|
||||||
|
path, handshake_scripts=getHandshakeScript(request.host), sites=SITES
|
||||||
|
)
|
||||||
|
|
||||||
# Try with .html
|
# Try with .html
|
||||||
if os.path.isfile("templates/" + path + ".html"):
|
if os.path.isfile("templates/" + path + ".html"):
|
||||||
return render_template(
|
return render_template(
|
||||||
path + ".html", handshake_scripts=getHandshakeScript(request.host), sites=SITES
|
path + ".html",
|
||||||
|
handshake_scripts=getHandshakeScript(request.host),
|
||||||
|
sites=SITES,
|
||||||
)
|
)
|
||||||
|
|
||||||
if os.path.isfile("templates/" + path.strip("/") + ".html"):
|
if os.path.isfile("templates/" + path.strip("/") + ".html"):
|
||||||
return render_template(
|
return render_template(
|
||||||
path.strip("/") + ".html", handshake_scripts=getHandshakeScript(request.host), sites=SITES
|
path.strip("/") + ".html",
|
||||||
|
handshake_scripts=getHandshakeScript(request.host),
|
||||||
|
sites=SITES,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Try to find a file matching
|
# Try to find a file matching
|
||||||
@@ -739,6 +697,7 @@ def catch_all(path: str):
|
|||||||
def not_found(e):
|
def not_found(e):
|
||||||
return error_response(request)
|
return error_response(request)
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
templates/assets/css/brand-reveal.min.css
vendored
2
templates/assets/css/brand-reveal.min.css
vendored
@@ -1 +1 @@
|
|||||||
.name-container{display:inline-flex;align-items:center;overflow:hidden;position:absolute;width:fit-content;left:50%;transform:translateX(-50%)}.slider{position:relative;left:0;animation:1s linear 1s forwards slide}@keyframes slide{0%{left:0}100%{left:calc(100%)}}.brand{mask-image:linear-gradient(to right,black 50%,transparent 50%);-webkit-mask-image:linear-gradient(to right,black 50%,transparent 50%);mask-position:100% 0;-webkit-mask-position:100% 0;mask-size:200%;-webkit-mask-size:200%;animation:1s linear 1s forwards reveal}@keyframes reveal{0%{mask-position:100% 0;-webkit-mask-position:100% 0}100%{mask-position:0 0;-webkit-mask-position:0 0}}.now-playing{position:fixed;bottom:0;right:0;border-top-left-radius:10px;background:#10101039;padding:1em}
|
.name-container{display:inline-flex;align-items:center;overflow:hidden;position:absolute;width:fit-content;left:50%;transform:translateX(-50%)}.slider{position:relative;left:0;animation:1s linear 1s forwards slide}@keyframes slide{0%{left:0}100%{left:calc(100%)}}.brand{mask-image:linear-gradient(to right,black 50%,transparent 50%);-webkit-mask-image:linear-gradient(to right,black 50%,transparent 50%);mask-position:100% 0;-webkit-mask-position:100% 0;mask-size:200%;-webkit-mask-size:200%;animation:1s linear 1s forwards reveal}@keyframes reveal{0%{mask-position:100% 0;-webkit-mask-position:100% 0}100%{mask-position:0 0;-webkit-mask-position:0 0}}.now-playing{position:fixed;bottom:0;right:0;border-top-left-radius:10px;background:#10101039;padding:1em}.hr-l{width:80%;border-width:2px;border-color:var(--bs-light);margin-top:0;opacity:.8}.hr-l-primary{border-width:3px;border-color:var(--bs-primary);margin-top:0;opacity:1}.float-right{position:absolute;right:3em}
|
||||||
104
templates/assets/css/resume-print.css
Normal file
104
templates/assets/css/resume-print.css
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
/* print.css */
|
||||||
|
@media print {
|
||||||
|
|
||||||
|
/* Page margins */
|
||||||
|
@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 10mm 10mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset body */
|
||||||
|
body, html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 10pt; /* smaller for print */
|
||||||
|
line-height: 1.3;
|
||||||
|
background: #fff !important;
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container adjustments */
|
||||||
|
.container-fluid, .resume-row, .resume-column {
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* background: none !important; */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flex layout for 33/66 split */
|
||||||
|
.resume-row {
|
||||||
|
display: flex !important;
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resume-column-left {
|
||||||
|
flex: 0 0 33.3333% !important;
|
||||||
|
max-width: 33.3333% !important;
|
||||||
|
padding-left: 5mm !important;
|
||||||
|
padding-right: 5mm !important;
|
||||||
|
color: #fff !important;
|
||||||
|
border: none !important;
|
||||||
|
break-inside: avoid !important;
|
||||||
|
page-break-inside: avoid !important;
|
||||||
|
}
|
||||||
|
.resume-column-left a {
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resume-column-right {
|
||||||
|
flex: 0 0 66.6667% !important;
|
||||||
|
max-width: 66.6667% !important;
|
||||||
|
padding-left: 5mm !important;
|
||||||
|
padding-right: 5mm !important;
|
||||||
|
background: #fff !important;
|
||||||
|
color: #000 !important;
|
||||||
|
border: none !important;
|
||||||
|
break-inside: avoid !important;
|
||||||
|
page-break-inside: avoid !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Images adjustments */
|
||||||
|
img {
|
||||||
|
max-width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
display: block;
|
||||||
|
margin: 10mm auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text adjustments for print */
|
||||||
|
h1 { font-size: 14pt; margin-bottom: 3mm; }
|
||||||
|
h2 { font-size: 12pt; margin-bottom: 2mm; }
|
||||||
|
h3 { font-size: 11pt; margin-bottom: 2mm; }
|
||||||
|
h4 { font-size: 10pt; margin-bottom: 1mm; }
|
||||||
|
h5, h6 { font-size: 9pt; margin-bottom: 1mm; }
|
||||||
|
p, li, .r-body, .l-body { font-size: 10pt; line-height: 1.3; }
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 36px !important;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
font-size: 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.r-heading1 {
|
||||||
|
margin-top: 4mm !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links as plain text */
|
||||||
|
a {
|
||||||
|
color: #000 !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Avoid page breaks inside blocks */
|
||||||
|
.noprintbreak {
|
||||||
|
break-inside: avoid !important;
|
||||||
|
page-break-inside: avoid !important;
|
||||||
|
/* margin-bottom: 5mm !important; */
|
||||||
|
}
|
||||||
|
.r-body {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
templates/assets/css/resume.min.css
vendored
2
templates/assets/css/resume.min.css
vendored
@@ -1 +1 @@
|
|||||||
.profile-container{height:170px;width:170px;z-index:2;left:10%}.title{position:absolute;margin-left:calc(100px);width:calc(100% - 100px);padding:1em;margin-top:-225px;z-index:0}.title>*{width:100%;margin-bottom:0}img.profile{left:10px;width:150px;position:absolute;aspect-ratio:1;transform:scale(1);transition:.5s;z-index:2}img.background2{left:0;width:170px!important;margin-top:-10px;pointer-events:none;z-index:1}img.foreground{border-radius:50%;pointer-events:none;z-index:3}img.background:hover,img.backgroundsml:hover{filter:blur(5px)}.spacer{height:100px}img.profilesml{width:150px;position:absolute;left:50%;margin-left:-85px;aspect-ratio:1;padding-top:calc(var(--s)/5);transform:scale(1);transition:.5s}img.foregroundsml{border-radius:50%;pointer-events:none}img.background2sml{width:170px!important;left:calc(50% - 10px);margin-top:-10px;pointer-events:none;z-index:0}print_text{color:#000!important}@media print{.noprintbreak{page-break-inside:avoid}*{color:#000;background-color:#fff}body{background-color:#fff}.hideprint{display:none}.print_text{color:#000!important}.profile-container{margin-top:10px!important}.r-heading1{font-size:16pt!important;margin-bottom:10px!important}.r-heading2{font-size:14pt!important}.r-heading3{font-size:12pt!important}.r-body,.r-small{font-size:10pt!important}.spacer{height:25px!important}}.r-heading1{margin-bottom:20px}.r-heading2{margin-bottom:0}.r-heading3{margin-bottom:.5em}@media (max-width:500px){.print_text{font-size:10px}}
|
img.profile-side{width:200px;aspect-ratio:1;z-index:2;border:6px solid #fff;margin:3em 0;border-radius:50%}.spacer{height:100px}.l-heading1,.l-heading2,.r-heading2{margin-bottom:0}.l-heading3,.r-heading3{margin-bottom:.5em}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{text-transform:none}.side-column{margin-top:2em}.noprintbreak{margin-bottom:1.5em}.resume-column-left{background:var(--bs-primary);padding-left:3em;padding-right:3em;max-width:320px}.resume-column-right{padding-right:3em;padding-left:3em;background:var(--bs-light);color:var(--bs-black)}.row-fill div{padding:0}.r-heading1{font-size:28px;margin-bottom:0;color:var(--bs-primary)}.title-hr{width:15%;color:var(--bs-primary);border-width:5px;border-color:var(--bs-primary);opacity:1}.l-body{margin-left:1em;line-height:initial}.r-body{line-height:initial}.l-summary{margin-top:3em}::selection{color:#fff;background-color:#0c4279}body{max-width:1400px;margin:0 auto}
|
||||||
2
templates/assets/css/styles.min.css
vendored
2
templates/assets/css/styles.min.css
vendored
@@ -1 +1 @@
|
|||||||
:root,[data-bs-theme=light]{--bs-primary:#6E0E9C;--bs-primary-rgb:110,14,156;--bs-primary-text-emphasis:#2C063E;--bs-primary-bg-subtle:#E2CFEB;--bs-primary-border-subtle:#C59FD7;--bs-link-color:#6E0E9C;--bs-link-color-rgb:110,14,156;--bs-link-hover-color:#a41685;--bs-link-hover-color-rgb:164,22,133}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#6E0E9C;--bs-btn-border-color:#6E0E9C;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5E0C85;--bs-btn-hover-border-color:#580B7D;--bs-btn-focus-shadow-rgb:233,219,240;--bs-btn-active-color:#fff;--bs-btn-active-bg:#580B7D;--bs-btn-active-border-color:#530B75;--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6E0E9C;--bs-btn-disabled-border-color:#6E0E9C}.btn-outline-primary{--bs-btn-color:#6E0E9C;--bs-btn-border-color:#6E0E9C;--bs-btn-focus-shadow-rgb:110,14,156;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6E0E9C;--bs-btn-hover-border-color:#6E0E9C;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6E0E9C;--bs-btn-active-border-color:#6E0E9C;--bs-btn-disabled-color:#6E0E9C;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6E0E9C}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}@media (min-width:992px){.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}}
|
:root,[data-bs-theme=light]{--bs-primary:#6E0E9C;--bs-primary-rgb:110,14,156;--bs-primary-text-emphasis:#2C063E;--bs-primary-bg-subtle:#E2CFEB;--bs-primary-border-subtle:#C59FD7;--bs-link-color:#6E0E9C;--bs-link-color-rgb:110,14,156;--bs-link-hover-color:#a41685;--bs-link-hover-color-rgb:164,22,133}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#6E0E9C;--bs-btn-border-color:#6E0E9C;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5E0C85;--bs-btn-hover-border-color:#580B7D;--bs-btn-focus-shadow-rgb:233,219,240;--bs-btn-active-color:#fff;--bs-btn-active-bg:#580B7D;--bs-btn-active-border-color:#530B75;--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6E0E9C;--bs-btn-disabled-border-color:#6E0E9C}.btn-outline-primary{--bs-btn-color:#6E0E9C;--bs-btn-border-color:#6E0E9C;--bs-btn-focus-shadow-rgb:110,14,156;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6E0E9C;--bs-btn-hover-border-color:#6E0E9C;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6E0E9C;--bs-btn-active-border-color:#6E0E9C;--bs-btn-disabled-color:#6E0E9C;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6E0E9C}
|
||||||
1
templates/assets/css/tools.min.css
vendored
Normal file
1
templates/assets/css/tools.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.card:hover{transform:translateY(-5px);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);transition:transform .2s,box-shadow .2s}.btn:hover{transform:scale(1.05);transition:transform .2s}
|
||||||
9
templates/error.ascii
Normal file
9
templates/error.ascii
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{{header}}
|
||||||
|
[1;36m───────────────────────────────────────────────[0m
|
||||||
|
[1;31m ERROR: {{ error.code }} [0m
|
||||||
|
[1;36m────────────[0m
|
||||||
|
|
||||||
|
[1;31m{{ error.message }}[0m
|
||||||
|
|
||||||
|
If you believe this is an error, please contact me via my socials listed at /contact
|
||||||
|
|
||||||
@@ -9,4 +9,5 @@ Contact [/contact]
|
|||||||
Projects [/projects]
|
Projects [/projects]
|
||||||
Tools [/tools]
|
Tools [/tools]
|
||||||
Donate [/donate]
|
Donate [/donate]
|
||||||
|
Now [/now]
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ Contact [/contact]
|
|||||||
Projects [/projects]
|
Projects [/projects]
|
||||||
Tools [/tools]
|
Tools [/tools]
|
||||||
Donate [/donate]
|
Donate [/donate]
|
||||||
API [/api/v1/]
|
Now [/now]
|
||||||
|
API [/api/v1]
|
||||||
|
|
||||||
[1;36m───────────────────────────────────────────────[0m
|
[1;36m───────────────────────────────────────────────[0m
|
||||||
[1;36m ABOUT ME [0m
|
[1;36m ABOUT ME [0m
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.
|
|||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 18C14.4183 18 18 14.4183 18 10C18 5.58172 14.4183 2 10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18ZM11 6C11 5.44772 10.5523 5 10 5C9.44771 5 9 5.44772 9 6V10C9 10.2652 9.10536 10.5196 9.29289 10.7071L12.1213 13.5355C12.5118 13.9261 13.145 13.9261 13.5355 13.5355C13.9261 13.145 13.9261 12.5118 13.5355 12.1213L11 9.58579V6Z" fill="currentColor"></path>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 18C14.4183 18 18 14.4183 18 10C18 5.58172 14.4183 2 10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18ZM11 6C11 5.44772 10.5523 5 10 5C9.44771 5 9 5.44772 9 6V10C9 10.2652 9.10536 10.5196 9.29289 10.7071L12.1213 13.5355C12.5118 13.9261 13.145 13.9261 13.5355 13.5355C13.9261 13.145 13.9261 12.5118 13.5355 12.1213L11 9.58579V6Z" fill="currentColor"></path>
|
||||||
</svg><span style="margin-left: 10px;font-family: 'Anonymous Pro', monospace;">{{time|safe}}</span></div><!-- Pop-out button for mobile -->
|
</svg><span style="margin-left: 10px;font-family: 'Anonymous Pro', monospace;">{{time|safe}}</span></div><!-- Pop-out button for mobile -->
|
||||||
<button id="spotify-toggle" style="
|
<button id="spotify-toggle" style="
|
||||||
display: none;
|
display: block;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
@@ -305,6 +305,8 @@ Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.
|
|||||||
background: none;
|
background: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
transform: translateX(200%); /* start hidden off-screen *
|
||||||
">
|
">
|
||||||
<img src="/assets/img/external/spotify.png" alt="Spotify" style="
|
<img src="/assets/img/external/spotify.png" alt="Spotify" style="
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -360,6 +362,21 @@ Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
"></div>
|
"></div>
|
||||||
|
<!-- Progress Bar -->
|
||||||
|
<div style="
|
||||||
|
margin-top: 6px;
|
||||||
|
height: 4px;
|
||||||
|
background: #333;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
">
|
||||||
|
<div id="spotify-progress" style="
|
||||||
|
width: 0%;
|
||||||
|
height: 100%;
|
||||||
|
background: #1DB954;
|
||||||
|
transition: width 1s linear;
|
||||||
|
"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -374,16 +391,17 @@ function isMobile() {
|
|||||||
function updateVisibility() {
|
function updateVisibility() {
|
||||||
if(isMobile()){
|
if(isMobile()){
|
||||||
widget.style.transform = 'translateX(120%)'; // hidden off-screen
|
widget.style.transform = 'translateX(120%)'; // hidden off-screen
|
||||||
toggleBtn.style.display = 'block';
|
toggleBtn.style.transform = 'translateX(0)'; // visible
|
||||||
} else {
|
} else {
|
||||||
widget.style.transform = 'translateX(0)'; // visible
|
widget.style.transform = 'translateX(0)'; // visible
|
||||||
toggleBtn.style.display = 'none';
|
toggleBtn.style.transform = 'translateX(200%)'; // hidden off-screen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle widget slide in/out on mobile
|
// Toggle widget slide in/out on mobile
|
||||||
toggleBtn.addEventListener('click', (e) => {
|
toggleBtn.addEventListener('click', (e) => {
|
||||||
widget.style.transform = 'translateX(0)'; // slide in
|
widget.style.transform = 'translateX(0)'; // slide in
|
||||||
|
toggleBtn.style.transform = 'translateX(200%)'; // hide button
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -392,6 +410,7 @@ document.addEventListener('click', (e) => {
|
|||||||
if(isMobile()){
|
if(isMobile()){
|
||||||
if(!widget.contains(e.target) && e.target !== toggleBtn){
|
if(!widget.contains(e.target) && e.target !== toggleBtn){
|
||||||
widget.style.transform = 'translateX(120%)'; // slide out
|
widget.style.transform = 'translateX(120%)'; // slide out
|
||||||
|
toggleBtn.style.transform = 'translateX(0)'; // show button
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -401,10 +420,19 @@ widget.addEventListener('click', (e) => {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Variable to track progress bar animation
|
||||||
|
let progressInterval = null;
|
||||||
|
let progressSpeed = 0;
|
||||||
|
let lastUpdateTime = Date.now();
|
||||||
|
let currentProgress = 0;
|
||||||
|
let targetProgress = 0;
|
||||||
|
let trackDuration = 0;
|
||||||
|
let currentTrackId = null;
|
||||||
|
|
||||||
// --- Spotify fetch ---
|
// --- Spotify fetch ---
|
||||||
async function updateSpotifyWidget() {
|
async function updateSpotifyWidget() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/spotify/');
|
const res = await fetch('/api/v1/playing');
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -419,6 +447,11 @@ async function updateSpotifyWidget() {
|
|||||||
document.getElementById('spotify-song').textContent = 'Not Playing';
|
document.getElementById('spotify-song').textContent = 'Not Playing';
|
||||||
document.getElementById('spotify-artist').textContent = '';
|
document.getElementById('spotify-artist').textContent = '';
|
||||||
document.getElementById('spotify-album').textContent = '';
|
document.getElementById('spotify-album').textContent = '';
|
||||||
|
document.getElementById('spotify-progress').style.width = '0%';
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
progressInterval = null;
|
||||||
|
currentProgress = 0;
|
||||||
|
currentTrackId = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,13 +462,52 @@ async function updateSpotifyWidget() {
|
|||||||
firstLoad = true;
|
firstLoad = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if track has changed (new song started)
|
||||||
|
const trackId = track.song_name + track.artist; // Simple track identifier
|
||||||
|
const isNewTrack = currentTrackId !== null && currentTrackId !== trackId;
|
||||||
|
|
||||||
|
if (isNewTrack) {
|
||||||
|
// Reset progress bar instantly for new track
|
||||||
|
currentProgress = 0;
|
||||||
|
document.getElementById('spotify-progress').style.transition = 'none';
|
||||||
|
document.getElementById('spotify-progress').style.width = '0%';
|
||||||
|
// Force reflow to apply the instant reset
|
||||||
|
document.getElementById('spotify-progress').offsetHeight;
|
||||||
|
// Re-enable transition
|
||||||
|
document.getElementById('spotify-progress').style.transition = 'width 0.1s linear';
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTrackId = trackId;
|
||||||
|
|
||||||
|
|
||||||
document.getElementById('spotify-album-art').src = track.album_art;
|
document.getElementById('spotify-album-art').src = track.album_art;
|
||||||
document.getElementById('spotify-song').textContent = track.song_name;
|
document.getElementById('spotify-song').textContent = track.song_name;
|
||||||
document.getElementById('spotify-artist').textContent = track.artist;
|
document.getElementById('spotify-artist').textContent = track.artist;
|
||||||
document.getElementById('spotify-album').textContent = track.album_name;
|
document.getElementById('spotify-album').textContent = track.album_name;
|
||||||
|
|
||||||
|
// Update progress bar
|
||||||
|
if (track.is_playing) {
|
||||||
|
currentProgress = (track.progress_ms / track.duration_ms) * 100;
|
||||||
|
trackDuration = track.duration_ms;
|
||||||
|
lastUpdateTime = Date.now();
|
||||||
|
|
||||||
|
document.getElementById('spotify-progress').style.width = currentProgress + '%';
|
||||||
|
|
||||||
|
// Clear existing interval
|
||||||
|
if (progressInterval) {
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
}
|
||||||
|
// Start interval to animate progress bar
|
||||||
|
progressInterval = setInterval(animateProgressBar, 100);
|
||||||
|
} else {
|
||||||
|
document.getElementById('spotify-progress').style.width = currentProgress + '%';
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
progressInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If first load and desktop, slide in the widget
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
widget.style.transform = 'translateX(0)'; // slide in on first load
|
updateVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -443,6 +515,30 @@ async function updateSpotifyWidget() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Animate progress bar
|
||||||
|
function animateProgressBar() {
|
||||||
|
if (trackDuration === 0) return;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsed = now - lastUpdateTime;
|
||||||
|
lastUpdateTime = now;
|
||||||
|
|
||||||
|
// Calculate progress increment based on elapsed time
|
||||||
|
const progressIncrement = (elapsed / trackDuration) * 100;
|
||||||
|
currentProgress += progressIncrement;
|
||||||
|
|
||||||
|
if (currentProgress >= 100) {
|
||||||
|
currentProgress = 100;
|
||||||
|
document.getElementById('spotify-progress').style.width = '100%';
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
progressInterval = null;
|
||||||
|
// Refresh API when progress reaches 100%
|
||||||
|
setTimeout(updateSpotifyWidget, 500);
|
||||||
|
} else {
|
||||||
|
document.getElementById('spotify-progress').style.width = currentProgress + '%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for Spotify API to have responded before initial display
|
// Wait for Spotify API to have responded before initial display
|
||||||
updateSpotifyWidget();
|
updateSpotifyWidget();
|
||||||
|
|
||||||
|
|||||||
6
templates/now.ascii
Normal file
6
templates/now.ascii
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{{header}}
|
||||||
|
[1;36m───────────────────────────────────────────────[0m
|
||||||
|
[1;36m Now {{ date }} [0m
|
||||||
|
[1;36m────────────[0m
|
||||||
|
|
||||||
|
{{content | safe}}
|
||||||
167
templates/now/25_11_20.html
Normal file
167
templates/now/25_11_20.html
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html data-bs-theme="light" lang="en-au" style="background: black;height: auto;">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||||
|
<title>What's up at the moment | Nathan.Woodburn/</title>
|
||||||
|
<meta name="theme-color" content="#000000">
|
||||||
|
<link rel="canonical" href="https://nathan.woodburn.au/now/25_11_20">
|
||||||
|
<meta property="og:url" content="https://nathan.woodburn.au/now/25_11_20">
|
||||||
|
<meta name="fediverse:creator" content="@nathanwoodburn@mastodon.woodburn.au">
|
||||||
|
<meta name="twitter:card" content="summary">
|
||||||
|
<meta name="twitter:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
|
||||||
|
<meta property="og:description" content="G'day,
|
||||||
|
Find out what I've been up to in the last little bit">
|
||||||
|
<meta name="twitter:title" content="What's up at the moment | Nathan.Woodburn/">
|
||||||
|
<meta property="og:title" content="What's up at the moment | Nathan.Woodburn/">
|
||||||
|
<meta name="description" content="G'day,
|
||||||
|
Find out what I've been up to in the last little bit">
|
||||||
|
<meta name="twitter:description" content="G'day,
|
||||||
|
Find out what I've been up to in the last little bit">
|
||||||
|
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/img/favicon/favicon-16x16.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/assets/img/favicon/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="192x192" href="/assets/img/favicon/android-chrome-192x192.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="512x512" href="/assets/img/favicon/android-chrome-512x512.png">
|
||||||
|
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
|
||||||
|
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic&display=swap">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&display=swap">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&display=swap">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap">
|
||||||
|
<link rel="stylesheet" href="/assets/fonts/font-awesome.min.css">
|
||||||
|
<link rel="stylesheet" href="/assets/fonts/ionicons.min.css">
|
||||||
|
<link rel="stylesheet" href="/assets/css/styles.min.css">
|
||||||
|
<link rel="stylesheet" href="/assets/css/brand-reveal.min.css">
|
||||||
|
<link rel="stylesheet" href="/assets/css/profile.min.css">
|
||||||
|
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
|
||||||
|
<link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" />
|
||||||
|
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="text-center" style="background: linear-gradient(rgba(0,0,0,0.80), rgba(0,0,0,0.80)), url("/assets/img/bg/background.webp") center / cover no-repeat;">
|
||||||
|
<nav class="navbar navbar-expand-md fixed-top navbar-light" id="mainNav" style="background: var(--bs-navbar-hover-color);">
|
||||||
|
<div class="container-fluid"><a class="navbar-brand" href="/#">
|
||||||
|
<div style="padding-right: 1em;display: inline-flex;">
|
||||||
|
<div class="slider"><span>/</span></div><span class="brand">Nathan.Woodburn</span>
|
||||||
|
</div>
|
||||||
|
</a><button data-bs-toggle="collapse" class="navbar-toggler navbar-toggler-right" data-bs-target="#navbarResponsive" type="button" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation" value="Menu"><i class="fa fa-bars"></i></button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarResponsive">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
|
||||||
|
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
|
||||||
|
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
|
||||||
|
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
|
||||||
|
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
|
||||||
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>{{handshake_scripts | safe}}
|
||||||
|
<div style="height: 10em;"></div>
|
||||||
|
<div class="profile-container" style="margin-bottom: 2em;"><img class="profile background" src="/assets/img/profile.jpg" style="border-radius: 50%;"><img class="profile foreground" src="/assets/img/pfront.webp"></div>
|
||||||
|
<h1 class="nathanwoodburn" style="margin-bottom: 0px;">Nathan.Woodburn/</h1>
|
||||||
|
<h3 style="margin-bottom: 0px;">WHat's Happening Now</h3>
|
||||||
|
<h6>{{DATE}}</h6>
|
||||||
|
<section style="margin-bottom: 50px;max-width: 95%;margin-right: auto;margin-left: auto;">
|
||||||
|
<div style="max-width: 700px;margin: auto;">
|
||||||
|
<h1 style="margin-bottom: 0px;">Starting at CSIRO</h1>
|
||||||
|
<p>I’m excited to share that I'm starting a new position at CSIRO as a Web Hosting System Administrator. It’s a role that sits right at the intersection of technology, security, and supporting the research happening across the organisation.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section style="margin-bottom: 50px;max-width: 95%;margin-right: auto;margin-left: auto;">
|
||||||
|
<div style="max-width: 700px;margin: auto;">
|
||||||
|
<h1 style="margin-bottom: 0px;">Website Updates</h1>
|
||||||
|
<p>I've updated my python3 flask website code to use UV for the package manager. It has cut down the initial install and startup from over 30s to under 10. This also makes building the docker image quicker and more consistent.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="text-center content-section" id="contact" style="padding-top: 0px;padding-bottom: 3em;">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 d-none d-print-block d-sm-block d-md-block d-lg-block d-xl-block d-xxl-block mx-auto">
|
||||||
|
<div class="social-div">
|
||||||
|
<ul class="list-unstyled social-list">
|
||||||
|
<li class="social-link"><a href="https://twitter.com/woodburn_nathan" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-twitter-x icon">
|
||||||
|
<path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865l8.875 11.633Z"></path>
|
||||||
|
</svg></a></li>
|
||||||
|
<li class="social-link"><a href="https://github.com/Nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-github icon">
|
||||||
|
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"></path>
|
||||||
|
</svg></a></li>
|
||||||
|
<li class="social-link"><a href="mailto:about@nathan.woodburn.au" target="_blank"><i class="icon ion-email icon"></i></a></li>
|
||||||
|
<li class="social-link discord"><a href="https://l.woodburn.au/discord" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-discord icon">
|
||||||
|
<path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612"></path>
|
||||||
|
</svg></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="social-div">
|
||||||
|
<ul class="list-unstyled social-list">
|
||||||
|
<li class="social-link mastodon"><a href="https://mastodon.woodburn.au/@nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-mastodon icon">
|
||||||
|
<path d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"></path>
|
||||||
|
</svg></a></li>
|
||||||
|
<li class="social-link youtube"><a href="https://www.youtube.com/@nathanjwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-youtube icon">
|
||||||
|
<path d="M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 0 1 1.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.007 2.007 0 0 1-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.007 2.007 0 0 1-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31.4 31.4 0 0 1 0 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.007 2.007 0 0 1 1.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A99.788 99.788 0 0 1 7.858 2h.193zM6.4 5.209v4.818l4.157-2.408z"></path>
|
||||||
|
</svg></a></li>
|
||||||
|
<li class="social-link signal"><a href="/signalQR" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-signal icon">
|
||||||
|
<path d="m6.08.234.179.727a7.264 7.264 0 0 0-2.01.832l-.383-.643A7.9 7.9 0 0 1 6.079.234zm3.84 0L9.742.96a7.265 7.265 0 0 1 2.01.832l.388-.643A7.957 7.957 0 0 0 9.92.234zm-8.77 3.63a7.944 7.944 0 0 0-.916 2.215l.727.18a7.264 7.264 0 0 1 .832-2.01l-.643-.386zM.75 8a7.3 7.3 0 0 1 .081-1.086L.091 6.8a8 8 0 0 0 0 2.398l.74-.112A7.262 7.262 0 0 1 .75 8m11.384 6.848-.384-.64a7.23 7.23 0 0 1-2.007.831l.18.728a7.965 7.965 0 0 0 2.211-.919zM15.251 8c0 .364-.028.727-.082 1.086l.74.112a7.966 7.966 0 0 0 0-2.398l-.74.114c.054.36.082.722.082 1.086m.516 1.918-.728-.18a7.252 7.252 0 0 1-.832 2.012l.643.387a7.933 7.933 0 0 0 .917-2.219zm-6.68 5.25c-.72.11-1.453.11-2.173 0l-.112.742a7.99 7.99 0 0 0 2.396 0l-.112-.741zm4.75-2.868a7.229 7.229 0 0 1-1.537 1.534l.446.605a8.07 8.07 0 0 0 1.695-1.689l-.604-.45zM12.3 2.163c.587.432 1.105.95 1.537 1.537l.604-.45a8.06 8.06 0 0 0-1.69-1.691l-.45.604zM2.163 3.7A7.242 7.242 0 0 1 3.7 2.163l-.45-.604a8.06 8.06 0 0 0-1.691 1.69l.604.45zm12.688.163-.644.387c.377.623.658 1.3.832 2.007l.728-.18a7.931 7.931 0 0 0-.916-2.214M6.913.831a7.254 7.254 0 0 1 2.172 0l.112-.74a7.985 7.985 0 0 0-2.396 0l.112.74zM2.547 14.64 1 15l.36-1.549-.729-.17-.361 1.548a.75.75 0 0 0 .9.902l1.548-.357-.17-.734zM.786 12.612l.732.168.25-1.073A7.187 7.187 0 0 1 .96 9.74l-.727.18a8 8 0 0 0 .736 1.902l-.184.79zm3.5 1.623-1.073.25.17.731.79-.184c.6.327 1.239.574 1.902.737l.18-.728a7.197 7.197 0 0 1-1.962-.811l-.007.005zM8 1.5a6.502 6.502 0 0 0-6.498 6.502 6.516 6.516 0 0 0 .998 3.455l-.625 2.668L4.54 13.5a6.502 6.502 0 0 0 6.93-11A6.516 6.516 0 0 0 8 1.5"></path>
|
||||||
|
</svg></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8 d-block d-print-none d-sm-none d-md-none d-lg-none d-xl-none d-xxl-none mx-auto">
|
||||||
|
<div class="social-div">
|
||||||
|
<ul class="list-unstyled social-list-sml">
|
||||||
|
<li class="social-link-sml"><a href="https://twitter.com/woodburn_nathan" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-twitter-x icon-sml">
|
||||||
|
<path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865l8.875 11.633Z"></path>
|
||||||
|
</svg></a></li>
|
||||||
|
<li class="social-link-sml"><a href="https://github.com/Nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-github icon-sml">
|
||||||
|
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"></path>
|
||||||
|
</svg></a></li>
|
||||||
|
<li class="social-link-sml"><a href="mailto:about@nathan.woodburn.au" target="_blank"><i class="icon ion-email icon-sml"></i></a></li>
|
||||||
|
<li class="discord social-link-sml"><a href="https://l.woodburn.au/discord" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-discord icon-sml">
|
||||||
|
<path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612"></path>
|
||||||
|
</svg></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="social-div">
|
||||||
|
<ul class="list-unstyled social-list-sml">
|
||||||
|
<li class="mastodon social-link-sml"><a href="https://mastodon.woodburn.au/@nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-mastodon icon-sml">
|
||||||
|
<path d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"></path>
|
||||||
|
</svg></a></li>
|
||||||
|
<li class="youtube social-link-sml"><a href="https://www.youtube.com/@nathanjwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-youtube icon-sml">
|
||||||
|
<path d="M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 0 1 1.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.007 2.007 0 0 1-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.007 2.007 0 0 1-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31.4 31.4 0 0 1 0 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.007 2.007 0 0 1 1.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A99.788 99.788 0 0 1 7.858 2h.193zM6.4 5.209v4.818l4.157-2.408z"></path>
|
||||||
|
</svg></a></li>
|
||||||
|
<li class="signal social-link-sml"><a href="/signalQR" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-signal icon-sml">
|
||||||
|
<path d="m6.08.234.179.727a7.264 7.264 0 0 0-2.01.832l-.383-.643A7.9 7.9 0 0 1 6.079.234zm3.84 0L9.742.96a7.265 7.265 0 0 1 2.01.832l.388-.643A7.957 7.957 0 0 0 9.92.234zm-8.77 3.63a7.944 7.944 0 0 0-.916 2.215l.727.18a7.264 7.264 0 0 1 .832-2.01l-.643-.386zM.75 8a7.3 7.3 0 0 1 .081-1.086L.091 6.8a8 8 0 0 0 0 2.398l.74-.112A7.262 7.262 0 0 1 .75 8m11.384 6.848-.384-.64a7.23 7.23 0 0 1-2.007.831l.18.728a7.965 7.965 0 0 0 2.211-.919zM15.251 8c0 .364-.028.727-.082 1.086l.74.112a7.966 7.966 0 0 0 0-2.398l-.74.114c.054.36.082.722.082 1.086m.516 1.918-.728-.18a7.252 7.252 0 0 1-.832 2.012l.643.387a7.933 7.933 0 0 0 .917-2.219zm-6.68 5.25c-.72.11-1.453.11-2.173 0l-.112.742a7.99 7.99 0 0 0 2.396 0l-.112-.741zm4.75-2.868a7.229 7.229 0 0 1-1.537 1.534l.446.605a8.07 8.07 0 0 0 1.695-1.689l-.604-.45zM12.3 2.163c.587.432 1.105.95 1.537 1.537l.604-.45a8.06 8.06 0 0 0-1.69-1.691l-.45.604zM2.163 3.7A7.242 7.242 0 0 1 3.7 2.163l-.45-.604a8.06 8.06 0 0 0-1.691 1.69l.604.45zm12.688.163-.644.387c.377.623.658 1.3.832 2.007l.728-.18a7.931 7.931 0 0 0-.916-2.214M6.913.831a7.254 7.254 0 0 1 2.172 0l.112-.74a7.985 7.985 0 0 0-2.396 0l.112.74zM2.547 14.64 1 15l.36-1.549-.729-.17-.361 1.548a.75.75 0 0 0 .9.902l1.548-.357-.17-.734zM.786 12.612l.732.168.25-1.073A7.187 7.187 0 0 1 .96 9.74l-.727.18a8 8 0 0 0 .736 1.902l-.184.79zm3.5 1.623-1.073.25.17.731.79-.184c.6.327 1.239.574 1.902.737l.18-.728a7.197 7.197 0 0 1-1.962-.811l-.007.005zM8 1.5a6.502 6.502 0 0 0-6.498 6.502 6.516 6.516 0 0 0 .998 3.455l-.625 2.668L4.54 13.5a6.502 6.502 0 0 0 6.93-11A6.516 6.516 0 0 0 8 1.5"></path>
|
||||||
|
</svg></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<footer style="background: #110033;">
|
||||||
|
<div class="container text-center">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<p class="d-none d-print-inline-block d-sm-inline-block d-md-inline-block d-lg-inline-block d-xl-inline-block d-xxl-inline-block">Want to look at some past Now pages?<br>Check out <a href="/old">/old</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<p class="d-none d-print-inline-block d-sm-inline-block d-md-inline-block d-lg-inline-block d-xl-inline-block d-xxl-inline-block">This site is also available on<br><a href="https://learn.namebase.io/" target="_blank">Handshake</a> at <a href="https://nathan.woodburn">https://nathan.woodburn/</a></p>
|
||||||
|
<p class="copyright">Copyright © Nathan.Woodburn/ 2025</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
|
||||||
|
<script src="/assets/js/script.min.js"></script>
|
||||||
|
<script src="/assets/js/grayscale.min.js"></script>
|
||||||
|
<script src="/assets/js/hacker.min.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -35,233 +35,157 @@
|
|||||||
<link rel="stylesheet" href="/assets/css/resume.min.css">
|
<link rel="stylesheet" href="/assets/css/resume.min.css">
|
||||||
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
|
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
|
||||||
<link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" />
|
<link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" />
|
||||||
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
|
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script><link rel="stylesheet" href="/assets/css/resume-print.css" media="print">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body style="width: 90%;margin-left: 5%;margin-right: 5%;font-family: 'Noto Sans', sans-serif;">
|
<body style="font-family: 'Noto Sans', sans-serif;"><div id="mobile-pdf-notice" style="display: none; background: #0d6efd; color: white; padding: 1rem; text-align: center; position: fixed; top: 0; left: 0; right: 0; z-index: 9999;">
|
||||||
<div class="d-none d-lg-inline d-xl-inline d-xxl-inline">
|
<strong>Mobile detected!</strong>
|
||||||
<div class="profile-container" style="margin-top: 5em;margin-bottom: 5em;">
|
<a href="/resume.pdf" style="color: white; text-decoration: underline;">View PDF version instead</a>
|
||||||
<div style="background-color: var(--bs-primary);height: 170px;width: 170px;margin-top: -10px;pointer-events: none;z-index: 1;position: absolute;border-radius: 50%;"></div><img class="profile foreground hideprint" src="/assets/img/nathanwoodburn.jpeg" alt="">
|
</div>
|
||||||
</div>
|
<script>
|
||||||
<div class="title" style="text-align: right;background: var(--bs-primary);">
|
if (window.innerWidth <= 768) {
|
||||||
<h1>Nathan Woodburn</h1>
|
document.getElementById('mobile-pdf-notice').style.display = 'block';
|
||||||
<p><a href="https://github.com/nathanwoodburn" style="color: rgb(255,255,255);text-decoration: none;display: inline;" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-github">
|
}
|
||||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"></path>
|
</script>
|
||||||
</svg></a> <a href="https://linkedin.com/in/nathanwoodburn" style="color: rgb(255,255,255);text-decoration: none;display: inline;" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-linkedin">
|
<div class="container-fluid h-100">
|
||||||
<path d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854V1.146zm4.943 12.248V6.169H2.542v7.225h2.401m-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248-.822 0-1.359.54-1.359 1.248 0 .694.521 1.248 1.327 1.248h.016zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016a5.54 5.54 0 0 1 .016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225h2.4"></path>
|
<div class="row h-100 resume-row">
|
||||||
</svg></a> | <a href="mailto:contact@nathan.woodburn.au" style="color: rgb(255,255,255);text-decoration: none;display: inline;" target="_blank">contact@nathan.woodburn.au</a> | <a href="https://nathan.woodburn.au" style="color: rgb(255,255,255);text-decoration: none;display: inline;" target="_blank">https://nathan.woodburn.au</a></p>
|
<div class="col-md-4 resume-column resume-column-left">
|
||||||
|
<div class="row row-cols-1 row-fill">
|
||||||
|
<div class="col">
|
||||||
|
<div class="text-center"><img class="profile-side" src="/assets/img/nathanwoodburn.jpeg" alt=""></div>
|
||||||
|
<h1 class="l-heading1">Contact</h1>
|
||||||
|
<hr class="hr-l">
|
||||||
|
<div class="r-small"><a class="print_text" href="tel:+61493129562" style="color: rgb(255,255,255);text-decoration: none;" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-telephone-fill">
|
||||||
|
<path fill-rule="evenodd" d="M1.885.511a1.745 1.745 0 0 1 2.61.163L6.29 2.98c.329.423.445.974.315 1.494l-.547 2.19a.678.678 0 0 0 .178.643l2.457 2.457a.678.678 0 0 0 .644.178l2.189-.547a1.745 1.745 0 0 1 1.494.315l2.306 1.794c.829.645.905 1.87.163 2.611l-1.034 1.034c-.74.74-1.846 1.065-2.877.702a18.634 18.634 0 0 1-7.01-4.42 18.634 18.634 0 0 1-4.42-7.009c-.362-1.03-.037-2.137.703-2.877L1.885.511z"></path>
|
||||||
|
</svg> +61 493 129 562</a></div>
|
||||||
|
<div class="r-small"><a class="print_text" href="mailto:nathan@woodburn.au" style="color: rgb(255,255,255);text-decoration: none;" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-envelope-fill">
|
||||||
|
<path d="M.05 3.555A2 2 0 0 1 2 2h12a2 2 0 0 1 1.95 1.555L8 8.414.05 3.555ZM0 4.697v7.104l5.803-3.558zM6.761 8.83l-6.57 4.027A2 2 0 0 0 2 14h12a2 2 0 0 0 1.808-1.144l-6.57-4.027L8 9.586l-1.239-.757Zm3.436-.586L16 11.801V4.697l-5.803 3.546Z"></path>
|
||||||
|
</svg> nathan@woodburn.au</a></div>
|
||||||
|
<div class="r-small"><span class="print_text"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-pin-angle-fill">
|
||||||
|
<path d="M9.828.722a.5.5 0 0 1 .354.146l4.95 4.95a.5.5 0 0 1 0 .707c-.48.48-1.072.588-1.503.588-.177 0-.335-.018-.46-.039l-3.134 3.134a5.927 5.927 0 0 1 .16 1.013c.046.702-.032 1.687-.72 2.375a.5.5 0 0 1-.707 0l-2.829-2.828-3.182 3.182c-.195.195-1.219.902-1.414.707-.195-.195.512-1.22.707-1.414l3.182-3.182-2.828-2.829a.5.5 0 0 1 0-.707c.688-.688 1.673-.767 2.375-.72a5.922 5.922 0 0 1 1.013.16l3.134-3.133a2.772 2.772 0 0 1-.04-.461c0-.43.108-1.022.589-1.503a.5.5 0 0 1 .353-.146z"></path>
|
||||||
|
</svg> Canberra, ACT</span></div>
|
||||||
|
<div class="r-small"><a class="print_text" href="https://nathan.woodburn.au" style="color: rgb(255,255,255);text-decoration: none;" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-link-45deg">
|
||||||
|
<path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1.002 1.002 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4.018 4.018 0 0 1-.128-1.287z"></path>
|
||||||
|
<path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"></path>
|
||||||
|
</svg> nathan.woodburn.au</a></div>
|
||||||
|
<div class="r-small"><a class="print_text" href="https://github.com/nathanwoodburn" style="color: rgb(255,255,255);text-decoration: none;" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-github">
|
||||||
|
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"></path>
|
||||||
|
</svg> @nathanwoodburn</a></div>
|
||||||
|
<div class="r-small"><a class="print_text" href="https://linkedin.com/in/nathanwoodburn" style="color: rgb(255,255,255);text-decoration: none;" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-linkedin">
|
||||||
|
<path d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854V1.146zm4.943 12.248V6.169H2.542v7.225h2.401m-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248-.822 0-1.359.54-1.359 1.248 0 .694.521 1.248 1.327 1.248h.016zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016a5.54 5.54 0 0 1 .016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225h2.4"></path>
|
||||||
|
</svg> @nathanwoodburn</a></div>
|
||||||
|
<div>
|
||||||
|
<div style="text-align: center;margin-bottom: 25px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col side-column">
|
||||||
|
<h1 class="l-heading1">Education</h1>
|
||||||
|
<hr class="hr-l">
|
||||||
|
<div class="noprintbreak">
|
||||||
|
<h5 class="r-heading2">Bachelor of Computing</h5>
|
||||||
|
<h6 class="r-heading3">Australian National University<br>2022 - Present</h6>
|
||||||
|
</div>
|
||||||
|
<div class="noprintbreak">
|
||||||
|
<h5 class="r-heading2">Discovering Engineering</h5>
|
||||||
|
<h6 class="r-heading3">Australian National University<br>Years 11 - 12</h6>
|
||||||
|
<p class="l-body">Completed enrichment program in engineering disciplines, CAD modeling, and design thinking</p>
|
||||||
|
</div>
|
||||||
|
<div class="noprintbreak">
|
||||||
|
<h5 class="r-heading2">Home Educated</h5>
|
||||||
|
<h6 class="r-heading3">Self-Directed Learning</h6>
|
||||||
|
<p class="l-body">Developed passion for technology through independent exploration of programming, system administration, and server management.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col side-column">
|
||||||
|
<h1 class="l-heading1">Skills</h1>
|
||||||
|
<hr class="hr-l">
|
||||||
|
<div class="noprintbreak">
|
||||||
|
<ul class="r-body">
|
||||||
|
<li>Python 3</li>
|
||||||
|
<li>Git</li>
|
||||||
|
<li>Docker Containerization</li>
|
||||||
|
<li>DNS</li>
|
||||||
|
<li>Linux administration</li>
|
||||||
|
<li>Technical troubleshooting</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col resume-column resume-column-right">
|
||||||
|
<div style="margin: 3em;">
|
||||||
|
<h1 class="title" style="margin-bottom: 0px;">Nathan Woodburn</h1>
|
||||||
|
<h1 class="subtitle r-heading3" style="font-size: 25px;color: var(--bs-gray);">{% if support %}Technical Support Specialist{% else %}Linux Systems Administrator{% endif %}</h1>
|
||||||
|
<hr class="title-hr">
|
||||||
|
</div>
|
||||||
|
<div class="l-summary">
|
||||||
|
<h1 class="r-heading1">Summary</h1>
|
||||||
|
<hr class="hr-l-primary">
|
||||||
|
<p class="r-body">{% if support %}Technical Support Specialist with expertise in Linux, DNS, and network troubleshooting. Experienced in resolving critical domain and network issues, supporting end-users, and collaborating with engineering teams to ensure stable and secure systems. Skilled in Python automation to streamline repetitive tasks and improve operational efficiency.{% else %}System Administrator specializing in Linux, Docker, and server deployments. Experienced in managing Proxmox, networks, and CI/CD pipelines. Implementing Python automations to optimize system operations. Ability to deploy and maintain server environments, self-hosted services, and web applications while ensuring reliability, scalability, and security.{% endif %}</p>
|
||||||
|
</div>
|
||||||
|
<div class="row g-0 row-cols-1">
|
||||||
|
<div class="col">
|
||||||
|
<h1 class="r-heading1">Experience</h1>
|
||||||
|
<hr class="hr-l-primary">
|
||||||
|
<div class="noprintbreak">
|
||||||
|
<h4 class="l-heading2 float-right">Dec 2025 - Present</h4>
|
||||||
|
<h4 class="l-heading2">Web Hosting System Administrator</h4>
|
||||||
|
<h6 class="l-heading3">CSIRO - Canberra</h6>
|
||||||
|
<ul class="r-body">
|
||||||
|
<li>Configure and manage web services</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="noprintbreak">
|
||||||
|
<h4 class="l-heading2 float-right">Oct 2022 - Jun 2025</h4>
|
||||||
|
<h4 class="l-heading2">Technical Support Specialist</h4>
|
||||||
|
<h6 class="l-heading3">Namebase - Remote</h6>
|
||||||
|
<ul class="r-body">
|
||||||
|
<li>Provided technical support for users, focusing on domain setup, configuration, and troubleshooting.</li>
|
||||||
|
<li>Worked with engineering teams to report bugs and suggest product improvements.</li>
|
||||||
|
<li>Diagnosed complex DNS issues including nameserver propagation and zone file errors.</li>
|
||||||
|
<li>Gained hands-on experience with recursive and authoritative DNS, DNSSEC, and decentralized naming.</li>
|
||||||
|
<li>Engaged with the community through social platforms and represented Namebase at conferences.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="noprintbreak">
|
||||||
|
<h4 class="l-heading2 float-right">Feb 2020 - Dec 2023</h4>
|
||||||
|
<h4 class="l-heading2">Small Business Owner</h4>
|
||||||
|
<h6 class="l-heading3">Nathan 3D Printing Service</h6>
|
||||||
|
<ul class="r-body">
|
||||||
|
<li>Operated a custom 3D printing and CAD design business independently.</li>
|
||||||
|
<li>Handled client communication, design iteration, and order fulfillment.</li>
|
||||||
|
<li>Built end-to-end project management and technical design skills.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<h1 class="r-heading1">Projects</h1>
|
||||||
|
<hr class="hr-l-primary">
|
||||||
|
<div class="noprintbreak">
|
||||||
|
<h4 class="l-heading2">Server Lab</h4>
|
||||||
|
<h6 class="l-heading3">Proxmox, Networking, Linux, DNS</h6>
|
||||||
|
<ul class="r-body">
|
||||||
|
<li>Maintain a personal physical server running Proxmox hypervisor.</li>
|
||||||
|
<li>Host multiple virtual machines across three VLANs with isolated firewalls for enhanced security.</li>
|
||||||
|
<li>Provide DNS and recursive resolver hosting services for external users.</li>
|
||||||
|
<li>Host a suite of self-hosted services such as Gitea, Authentik, Vaultwarden and Nextcloud.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="noprintbreak">
|
||||||
|
<h4 class="l-heading2">Personal Website</h4>
|
||||||
|
<h6 class="l-heading3">Python 3, Flask, Docker, CI/CD</h6>
|
||||||
|
<ul class="r-body">
|
||||||
|
<li>Designed modular web application architecture with Flask blueprints and reusable templates.</li>
|
||||||
|
<li>Managed containerized deployment using Docker on a dedicated server, ensuring consistency and scalability.</li>
|
||||||
|
<li>Implemented CI/CD pipelines for automated testing, building, and deployment from Git.</li>
|
||||||
|
<li>Integrated dynamic content and interactive features while maintaining secure and optimized server operations.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-lg-none d-xl-none d-xxl-none">
|
|
||||||
<div class="profile-container" style="margin-top: 5em;margin-bottom: 10px;"><img class="profilesml foregroundsml" src="/assets/img/nathanwoodburn.jpeg" style="width: 170px;border: 10px solid var(--bs-primary) ;" alt=""></div>
|
|
||||||
<div style="text-align: center;margin-bottom: 25px;">
|
|
||||||
<h1 style="margin-bottom: 0px;">Nathan Woodburn</h1>
|
|
||||||
<div class="r-small"><a class="print_text" href="mailto:contact@nathan.woodburn.au" style="color: rgb(255,255,255);text-decoration: none;" target="_blank">contact@nathan.woodburn.au</a><span> | </span><a class="print_text" href="https://nathan.woodburn.au" style="color: rgb(255,255,255);text-decoration: none;" target="_blank">https://nathan.woodburn.au</a></div>
|
|
||||||
<div class="r-small"><a class="print_text" href="https://github.com/nathanwoodburn" style="color: rgb(255,255,255);text-decoration: none;" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-github">
|
|
||||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"></path>
|
|
||||||
</svg> @nathanwoodburn</a><span> | </span><a class="print_text" href="https://linkedin.com/in/nathanwoodburn" style="color: rgb(255,255,255);text-decoration: none;" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-linkedin">
|
|
||||||
<path d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854V1.146zm4.943 12.248V6.169H2.542v7.225h2.401m-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248-.822 0-1.359.54-1.359 1.248 0 .694.521 1.248 1.327 1.248h.016zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016a5.54 5.54 0 0 1 .016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225h2.4"></path>
|
|
||||||
</svg> @nathanwoodburn</a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="max-width: 2000px;margin: auto;">
|
|
||||||
<div style="margin-bottom: 50px;">
|
|
||||||
<h1 class="r-heading3" style="font-size: 25px;">Summary</h1>
|
|
||||||
<p class="r-body">Linux and server administration student with experience managing servers, DNS, virtualization, and networking. Skilled in deploying and maintaining self-hosted services, troubleshooting complex system issues, and building resilient, automated infrastructures. Passionate about open-source tools and practical system design.</p>
|
|
||||||
</div>
|
|
||||||
<div class="row row-cols-1 row-cols-lg-2 row-cols-xl-2 row-cols-xxl-2">
|
|
||||||
<div class="col">
|
|
||||||
<div class="noprintbreak">
|
|
||||||
<h1 class="r-heading1">Experience</h1>
|
|
||||||
<h4 class="r-heading2">Technical Support Specialist</h4>
|
|
||||||
<h6 class="r-heading3">Namebase - Remote | Oct 2022 - JUN 2025</h6>
|
|
||||||
<ul class="r-body">
|
|
||||||
<li>Provided technical support for users, focusing on domain setup, configuration, and troubleshooting.</li>
|
|
||||||
<li>Worked with engineering teams to report bugs and suggest product improvements.</li>
|
|
||||||
<li>Diagnosed complex DNS issues including nameserver propagation and zone file errors.</li>
|
|
||||||
<li>Gained hands-on experience with recursive and authoritative DNS, DNSSEC, and decentralized naming.</li>
|
|
||||||
<li>Engaged with the community through social platforms and represented Namebase at conferences.</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
<div class="noprintbreak">
|
|
||||||
<h4 class="r-heading2">Small Business Owner</h4>
|
|
||||||
<h6 class="r-heading3">Nathan 3D Printing Service | Feb 2020 - Dec 2023</h6>
|
|
||||||
<ul class="r-body">
|
|
||||||
<li>Operated a custom 3D printing and CAD design business independently.</li>
|
|
||||||
<li>Handled client communication, design iteration, and order fulfillment.</li>
|
|
||||||
<li>Built end-to-end project management and technical design skills.</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
<div class="noprintbreak">
|
|
||||||
<h4 class="r-heading2">Audio Production Volunteer</h4>
|
|
||||||
<h6 class="r-heading3">1WAY FM | Feb 2021 - Dec 2021</h6>
|
|
||||||
<ul class="r-body">
|
|
||||||
<li>Recorded, edited, and produced audio content for community radio broadcasts.</li>
|
|
||||||
<li>Supported the production team in day-to-day technical operations.</li>
|
|
||||||
<li>Gained practical skills in audio engineering and collaborative media work.</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col edu-main">
|
|
||||||
<div class="noprintbreak">
|
|
||||||
<h1 class="r-heading1">Education</h1>
|
|
||||||
<h4 class="r-heading2">Bachelor of Computing</h4>
|
|
||||||
<h6 class="r-heading3">Australian National University | 2022 - Present</h6>
|
|
||||||
<ul class="r-body">
|
|
||||||
<li>Currently pursuing a Bachelor of Computing with a specialization in cybersecurity.</li>
|
|
||||||
<li>Gaining hands-on experience in network security, system design, and secure software development.</li>
|
|
||||||
<li>Building a strong foundation in computer science principles, programming, and system architecture.</li>
|
|
||||||
<li>Collaborating on group projects and labs to apply theoretical knowledge to real-world challenges.</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
<div class="noprintbreak">
|
|
||||||
<h4 class="r-heading2">Discovering Engineering</h4>
|
|
||||||
<h6 class="r-heading3">Australian National University | YearS 11 & 12</h6>
|
|
||||||
<ul class="r-body">
|
|
||||||
<li>Completed an enrichment program introducing core engineering disciplines and technical concepts.</li>
|
|
||||||
<li>Explored CAD modeling, design thinking, and practical problem-solving through workshops and case studies.</li>
|
|
||||||
<li>Gained early exposure to engineering tools and technical communication, laying the groundwork for later technical studies.</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
<div class="noprintbreak">
|
|
||||||
<h4 class="r-heading2">Home Educated</h4>
|
|
||||||
<h6 class="r-heading3">Self-Directed Learning</h6>
|
|
||||||
<ul class="r-body">
|
|
||||||
<li>Cultivated time management, self-discipline, and critical thinking skills crucial for success in tech.</li>
|
|
||||||
<li>Developed a strong passion for technology, programming, and system administration through independent exploration.</li>
|
|
||||||
<li>Built custom applications, managed servers, and solved technical challenges in a flexible learning environment.</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="spacer"></div>
|
|
||||||
<div class="col">
|
|
||||||
<h1 class="r-heading1">Projects</h1>
|
|
||||||
<div class="noprintbreak">
|
|
||||||
<h4 class="r-heading2">Server Lab</h4>
|
|
||||||
<h6 class="r-heading3">Proxmox, Networking, Linux, DNS</h6>
|
|
||||||
<ul class="r-body">
|
|
||||||
<li>Maintain a personal physical server running Proxmox hypervisor.</li>
|
|
||||||
<li>Host multiple virtual machines across three VLANs with isolated firewalls for enhanced security.</li>
|
|
||||||
<li>Provide DNS and recursive resolver hosting services for external users.</li>
|
|
||||||
<li>Host a suite of self-hosted services such as Gitea, Authentik, Vaultwarden and Nextcloud.</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
<div class="noprintbreak">
|
|
||||||
<h4 class="r-heading2">HNSDoH</h4>
|
|
||||||
<h6 class="r-heading3">DNS, Handshake, DoH, Distributed Systems, Linux</h6>
|
|
||||||
<ul class="r-body">
|
|
||||||
<li>Manage a distributed Handshake DoH resolver network spanning six independent nodes.</li>
|
|
||||||
<li>Administer four nodes and collaborate with two external operators on updates, patches, and troubleshooting.</li>
|
|
||||||
<li>Ensure uptime and resiliency across geographically distributed infrastructure.</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
<div class="noprintbreak">
|
|
||||||
<h4 class="r-heading2">FireWallet</h4>
|
|
||||||
<h6 class="r-heading3">Python, Handshake, Plugin Architecture</h6>
|
|
||||||
<ul class="r-body">
|
|
||||||
<li>Developed a modular Python-based Handshake wallet with plugin support for extensibility.</li>
|
|
||||||
<li>Presented at HandyCon 2024 and 2025, showcasing usability improvements and HNS site resolution.</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="spacer"></div>
|
|
||||||
<div>
|
|
||||||
<div class="noprintbreak">
|
|
||||||
<h1 class="r-heading1">Skills</h1>
|
|
||||||
<h4 class="r-heading2">Programming & Development</h4>
|
|
||||||
<ul class="r-body">
|
|
||||||
<li><strong>Python 3</strong>: Proficient in building web services and automation tools; experienced with libraries such as Flask, requests, and asyncio.</li>
|
|
||||||
<li><strong>C & Java</strong>: Applied in university coursework and labs for systems programming, algorithms, and object-oriented design.</li>
|
|
||||||
<li><strong>C#</strong>: Experienced in building Windows applications, including debugging and testing since 2016.</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
<div class="noprintbreak">
|
|
||||||
<h4 class="r-heading2">Networking & Security</h4>
|
|
||||||
<ul class="r-body">
|
|
||||||
<li><strong>DNS & DNSSEC</strong>: Skilled in managing DNS zones, records, and DNSSEC; experienced with both authoritative and recursive resolvers.</li>
|
|
||||||
<li><strong>Linux System Administration</strong>: Manage cloud and physical servers, using the command line for scripting, security, and package management.</li>
|
|
||||||
<li><strong>Server Infrastructure</strong>: Operate a dedicated server running Proxmox; manage virtual machines across VLANs with separate firewalls to enhance isolation and security.</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
<div class="noprintbreak">
|
|
||||||
<h4 class="r-heading2">Technical Support & Communication</h4>
|
|
||||||
<ul class="r-body">
|
|
||||||
<li><strong>Technical Support</strong>: Deliver front-line technical assistance, troubleshoot software/platform issues, and communicate clearly with users.</li>
|
|
||||||
<li><strong>Community Engagement</strong>: Active contributor and presenter within the Handshake and blockchain communities.</li>
|
|
||||||
<li><strong>Tools</strong>: Git, Docker, NGINX, SSH, Bash scripting.</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="spacer"></div>
|
|
||||||
<div>
|
|
||||||
<h1 class="r-heading1">Conferences</h1>
|
|
||||||
<div class="noprintbreak">
|
|
||||||
<h4 class="r-heading2">Presenter – HandyCon 2025</h4>
|
|
||||||
<h6 class="r-heading3">Online | March 2025</h6>
|
|
||||||
<ul class="r-body">
|
|
||||||
<li><strong>Firewallet Updates & How to Resolve HNS Sites</strong> – Presented new features and usability improvements in FireWallet, including user-friendly Handshake resolution methods.</li>
|
|
||||||
<li><strong>Building the Future of Handshake: Advancing Wallets & Ecosystem Development</strong> (co-presented with Rithvik Vibhu) – Discussed strategies for wallet development, improving developer tooling, and enhancing the decentralized web experience on Handshake.</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
<div class="noprintbreak">
|
|
||||||
<h4 class="r-heading2">Judge & Speaker – Onchain Names & Identity Hackathon</h4>
|
|
||||||
<h6 class="r-heading3">Vietnam | April 2024</h6>
|
|
||||||
<ul class="r-body">
|
|
||||||
<li>Invited judge for blockchain-focused hackathon entries using Handshake and decentralized identity tools.</li>
|
|
||||||
<li>Delivered a talk comparing Handshake DNS with traditional DNS systems, highlighting benefits of decentralized roots for security and censorship resistance.</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
<div class="noprintbreak">
|
|
||||||
<h4 class="r-heading2">Presenter – HandyCon 2024</h4>
|
|
||||||
<h6 class="r-heading3">Online | March 2024</h6>
|
|
||||||
<ul class="r-body">
|
|
||||||
<li><strong>FireWallet</strong> – Showcased a modular Handshake wallet written in Python, designed with plugin support to enable extensibility and developer customization.</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
<div class="noprintbreak">
|
|
||||||
<h4 class="r-heading2">Presenter – HandyCon 2023</h4>
|
|
||||||
<h6 class="r-heading3">Online | March 2023</h6>
|
|
||||||
<ul class="r-body">
|
|
||||||
<li>Presented a technical walkthrough on launching websites with Handshake domains.</li>
|
|
||||||
<li>Covered HTTPS setup using DANE to eliminate reliance on traditional certificate authorities.</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="spacer"></div>
|
|
||||||
</div>
|
|
||||||
<footer class="text-center bg-dark d-print-none" style="width: 99vw;margin-left: -5vw;padding: 0px;">
|
|
||||||
<div class="container text-white py-4 py-lg-5" style="width: auto;max-width: 100%;">
|
|
||||||
<ul class="list-inline">
|
|
||||||
<li class="list-inline-item me-4"><a href="https://www.facebook.com/nathanjwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-facebook text-light">
|
|
||||||
<path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951"></path>
|
|
||||||
</svg></a></li>
|
|
||||||
<li class="list-inline-item me-4"><a href="https://twitter.com/woodburn_nathan" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-twitter text-light">
|
|
||||||
<path d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15"></path>
|
|
||||||
</svg></a></li>
|
|
||||||
<li class="list-inline-item me-4"><a href="https://github.com/nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-github text-light">
|
|
||||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"></path>
|
|
||||||
</svg></a></li>
|
|
||||||
</ul>
|
|
||||||
<p class="text-muted mb-0" style="color: rgb(255,255,255) !important;">Copyright © Nathan.Woodburn/ 2025</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
|
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
|
||||||
<script src="/assets/js/script.min.js"></script>
|
<script src="/assets/js/script.min.js"></script>
|
||||||
<script src="/assets/js/grayscale.min.js"></script>
|
<script src="/assets/js/grayscale.min.js"></script>
|
||||||
|
|||||||
@@ -72,6 +72,9 @@
|
|||||||
<url>
|
<url>
|
||||||
<loc>https://nathan.woodburn.au/now/25_10_23</loc>
|
<loc>https://nathan.woodburn.au/now/25_10_23</loc>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nathan.woodburn.au/now/25_11_20</loc>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://nathan.woodburn.au/now/old</loc>
|
<loc>https://nathan.woodburn.au/now/old</loc>
|
||||||
</url>
|
</url>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Here are some of the tools I use regularly — most of them are open source!
|
|||||||
[1;33m{{tool.name}}[0m
|
[1;33m{{tool.name}}[0m
|
||||||
{{tool.description}}
|
{{tool.description}}
|
||||||
Website: {{tool.url}}
|
Website: {{tool.url}}
|
||||||
{% if tool.demo_url %}Demo: {{tool.demo_url}}{% endif %}
|
{% if tool.demo %}Demo: {{tool.demo}}{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
[1;36m───────────────────────────────────────────────[0m
|
[1;36m───────────────────────────────────────────────[0m
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<link rel="stylesheet" href="/assets/css/brand-reveal.min.css">
|
<link rel="stylesheet" href="/assets/css/brand-reveal.min.css">
|
||||||
<link rel="stylesheet" href="/assets/css/profile.min.css">
|
<link rel="stylesheet" href="/assets/css/profile.min.css">
|
||||||
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
|
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
|
||||||
|
<link rel="stylesheet" href="/assets/css/tools.min.css">
|
||||||
<link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" />
|
<link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" />
|
||||||
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
|
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -76,11 +77,15 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
{% for tool in tools_in_type %}
|
{% for tool in tools_in_type %}
|
||||||
<div class="col-md-6 col-lg-4 mb-4">
|
<div class="col-md-6 col-lg-4 mb-4">
|
||||||
<div class="card h-100 shadow-sm transition-all" style="transition: transform 0.2s, box-shadow 0.2s;" onmouseover="this.style.transform='translateY(-5px)'; this.style.boxShadow='0 0.5rem 1rem rgba(0,0,0,0.15)';" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='';">
|
<div class="card h-100 shadow-sm transition-all" style="transition: transform 0.2s, box-shadow 0.2s;">
|
||||||
<div class="card-body d-flex flex-column">
|
<div class="card-body d-flex flex-column">
|
||||||
<h4 class="card-title">{{tool.name}}</h4>
|
<h4 class="card-title">{{tool.name}}</h4>
|
||||||
<p class="card-text">{{ tool.description }}</p>
|
<p class="card-text">{{ tool.description }}</p>
|
||||||
<div class="btn-group gap-3 mt-auto" role="group">{% if tool.demo %}<button class="btn btn-primary" type="button" data-bs-target="#modal-{{tool.name}}" data-bs-toggle="modal" style="transition: transform 0.2s, background-color 0.2s;" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">View Demo</button>{% endif %}<a class="btn btn-primary" role="button" href="{{tool.url}}" target="_blank" style="transition: transform 0.2s, background-color 0.2s;" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">{{tool.name}} Website</a></div>
|
<div class="btn-group gap-3 mt-auto" role="group">{% if tool.demo %}<button class="btn btn-primary"
|
||||||
|
type="button" data-bs-target="#modal-{{tool.name}}" data-bs-toggle="modal"
|
||||||
|
style="transition: transform 0.2s, background-color 0.2s;">View Demo</button>{% endif %}<a
|
||||||
|
class="btn btn-primary" role="button" href="{{tool.url}}" target="_blank"
|
||||||
|
style="transition: transform 0.2s, background-color 0.2s;">{{tool.name}} Website</a></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,49 +94,110 @@
|
|||||||
<!-- Modals for this type -->
|
<!-- Modals for this type -->
|
||||||
{% for tool in tools_in_type %}
|
{% for tool in tools_in_type %}
|
||||||
{% if tool.demo %}
|
{% if tool.demo %}
|
||||||
<div id="modal-{{tool.name}}" class="modal fade" role="dialog" tabindex="-1" style="z-index: 1055;">
|
<div id="modal-{{tool.name}}" class="modal fade" role="dialog" tabindex="-1" style="z-index: 1055;"
|
||||||
|
data-demo-url="{{ tool.demo | e }}">
|
||||||
<div class="modal-dialog modal-xl" role="document">
|
<div class="modal-dialog modal-xl" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title">{{tool.name}}</h4><button class="btn-close" type="button" aria-label="Close" data-bs-dismiss="modal"></button>
|
<h4 class="modal-title">{{tool.name}}</h4><button class="btn-close" type="button" aria-label="Close"
|
||||||
|
data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
|
||||||
{{ tool.demo | safe }}
|
<div class="modal-body" data-demo-loaded="false"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer"><button class="btn btn-light" type="button" data-bs-dismiss="modal">Close</button></div>
|
<div class="modal-footer"><button class="btn btn-light" type="button" data-bs-dismiss="modal">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const navbar = document.getElementById('mainNav');
|
const navbar = document.getElementById('mainNav');
|
||||||
const headers = document.querySelectorAll('.section-header');
|
const headers = document.querySelectorAll('.section-header');
|
||||||
|
|
||||||
if (navbar) {
|
if (navbar) {
|
||||||
const navbarHeight = navbar.offsetHeight;
|
const navbarHeight = navbar.offsetHeight;
|
||||||
headers.forEach(header => {
|
headers.forEach(header => {
|
||||||
header.style.top = navbarHeight + 'px';
|
header.style.top = navbarHeight + 'px';
|
||||||
header.style.zIndex = '100';
|
header.style.zIndex = '100';
|
||||||
header.style.scrollMarginTop = navbarHeight + 'px';
|
header.style.scrollMarginTop = navbarHeight + 'px';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle hash navigation on page load
|
// Handle hash navigation on page load
|
||||||
if (window.location.hash) {
|
if (window.location.hash) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const target = document.querySelector(window.location.hash);
|
const target = document.querySelector(window.location.hash);
|
||||||
if (target) {
|
if (target) {
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: target.offsetTop - navbarHeight,
|
top: target.offsetTop - navbarHeight,
|
||||||
behavior: 'smooth'
|
behavior: 'smooth'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Load demo in modal
|
||||||
|
document.querySelectorAll('.modal').forEach(modal => {
|
||||||
|
modal.addEventListener('show.bs.modal', () => {
|
||||||
|
const body = modal.querySelector('.modal-body');
|
||||||
|
if (body.dataset.demoLoaded === 'false') {
|
||||||
|
const demoUrl = modal.dataset.demoUrl;
|
||||||
|
const iframeId = 'iframe-' + modal.id;
|
||||||
|
|
||||||
|
// Add a div on top of all content to show loading message
|
||||||
|
const loadingDiv = document.createElement('div');
|
||||||
|
loadingDiv.style.position = 'absolute';
|
||||||
|
loadingDiv.style.top = '0';
|
||||||
|
loadingDiv.style.left = '0';
|
||||||
|
loadingDiv.style.width = '100%';
|
||||||
|
loadingDiv.style.height = '100%';
|
||||||
|
loadingDiv.style.backgroundColor = 'rgb(0, 0, 0)';
|
||||||
|
loadingDiv.style.display = 'flex';
|
||||||
|
loadingDiv.style.justifyContent = 'center';
|
||||||
|
loadingDiv.style.alignItems = 'center';
|
||||||
|
loadingDiv.style.zIndex = '10';
|
||||||
|
const loadingMsg = document.createElement('p');
|
||||||
|
loadingMsg.className = 'text-center';
|
||||||
|
loadingMsg.textContent = 'Loading demo...';
|
||||||
|
loadingDiv.appendChild(loadingMsg);
|
||||||
|
body.style.position = 'relative';
|
||||||
|
body.appendChild(loadingDiv);
|
||||||
|
|
||||||
|
// Create iframe
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.src = demoUrl + '/iframe';
|
||||||
|
iframe.id = iframeId;
|
||||||
|
iframe.style.width = '100%';
|
||||||
|
iframe.style.height = '400px'; // temporary height
|
||||||
|
iframe.style.border = '0';
|
||||||
|
iframe.setAttribute('scrolling', 'no');
|
||||||
|
iframe.setAttribute('allowfullscreen', 'true');
|
||||||
|
|
||||||
|
body.appendChild(iframe);
|
||||||
|
body.dataset.demoLoaded = 'true';
|
||||||
|
|
||||||
|
// Listen for bodySize message from asciinema iframe
|
||||||
|
const origin = new URL(demoUrl).origin;
|
||||||
|
function onMessage(event) {
|
||||||
|
if (event.origin !== origin || event.source !== iframe.contentWindow) return;
|
||||||
|
if (event.data.type === 'bodySize' && event.data.payload.height) {
|
||||||
|
iframe.style.height = event.data.payload.height + 'px';
|
||||||
|
// Remove loading message
|
||||||
|
body.removeChild(loadingDiv);
|
||||||
|
// Optional: limit modal max height
|
||||||
|
modal.querySelector('.modal-dialog').style.maxHeight = '90vh';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', onMessage, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script></div>
|
</script></div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -5,11 +5,16 @@ HTTP 200
|
|||||||
GET http://127.0.0.1:5000/api/v1/ip
|
GET http://127.0.0.1:5000/api/v1/ip
|
||||||
HTTP 200
|
HTTP 200
|
||||||
[Asserts]
|
[Asserts]
|
||||||
jsonpath "$.ip" == "127.0.0.1"
|
jsonpath "$.ip" matches "^(127|172).(0|17).0.1$"
|
||||||
|
|
||||||
GET http://127.0.0.1:5000/api/v1/time
|
GET http://127.0.0.1:5000/api/v1/time
|
||||||
HTTP 200
|
HTTP 200
|
||||||
GET http://127.0.0.1:5000/api/v1/timezone
|
GET http://127.0.0.1:5000/api/v1/timezone
|
||||||
HTTP 200
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
jsonpath "$.timezone" >= 10
|
||||||
|
jsonpath "$.timezone" <= 12
|
||||||
|
|
||||||
GET http://127.0.0.1:5000/api/v1/message
|
GET http://127.0.0.1:5000/api/v1/message
|
||||||
HTTP 200
|
HTTP 200
|
||||||
GET http://127.0.0.1:5000/api/v1/project
|
GET http://127.0.0.1:5000/api/v1/project
|
||||||
@@ -18,3 +23,6 @@ GET http://127.0.0.1:5000/api/v1/tools
|
|||||||
HTTP 200
|
HTTP 200
|
||||||
[Asserts]
|
[Asserts]
|
||||||
jsonpath "$.tools" count > 5
|
jsonpath "$.tools" count > 5
|
||||||
|
|
||||||
|
GET http://127.0.0.1:5000/api/v1/playing
|
||||||
|
HTTP 200
|
||||||
28
tools.py
28
tools.py
@@ -1,6 +1,6 @@
|
|||||||
from flask import Request, render_template, jsonify, make_response
|
from flask import Request, render_template, jsonify, make_response
|
||||||
import os
|
import os
|
||||||
from functools import lru_cache as cache
|
from functools import lru_cache
|
||||||
import datetime
|
import datetime
|
||||||
from typing import Optional, Dict, Union, Tuple
|
from typing import Optional, Dict, Union, Tuple
|
||||||
import re
|
import re
|
||||||
@@ -27,6 +27,15 @@ CRAWLERS = [
|
|||||||
"Twitterbot"
|
"Twitterbot"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
CLI_AGENTS = [
|
||||||
|
"curl",
|
||||||
|
"hurl",
|
||||||
|
"xh",
|
||||||
|
"Posting",
|
||||||
|
"HTTPie",
|
||||||
|
"nushell"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def getClientIP(request: Request) -> str:
|
def getClientIP(request: Request) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -47,7 +56,7 @@ def getClientIP(request: Request) -> str:
|
|||||||
ip = "unknown"
|
ip = "unknown"
|
||||||
return ip
|
return ip
|
||||||
|
|
||||||
@cache
|
@lru_cache(maxsize=1)
|
||||||
def getGitCommit() -> str:
|
def getGitCommit() -> str:
|
||||||
"""
|
"""
|
||||||
Get the current git commit hash.
|
Get the current git commit hash.
|
||||||
@@ -75,7 +84,7 @@ def getGitCommit() -> str:
|
|||||||
return "failed to get version"
|
return "failed to get version"
|
||||||
|
|
||||||
|
|
||||||
def isCurl(request: Request) -> bool:
|
def isCLI(request: Request) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if the request is from curl or hurl.
|
Check if the request is from curl or hurl.
|
||||||
|
|
||||||
@@ -87,7 +96,7 @@ def isCurl(request: Request) -> bool:
|
|||||||
"""
|
"""
|
||||||
if request.headers and request.headers.get("User-Agent"):
|
if request.headers and request.headers.get("User-Agent"):
|
||||||
user_agent = request.headers.get("User-Agent", "")
|
user_agent = request.headers.get("User-Agent", "")
|
||||||
return "curl" in user_agent or "hurl" in user_agent
|
return any(agent in user_agent for agent in CLI_AGENTS)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -106,7 +115,7 @@ def isCrawler(request: Request) -> bool:
|
|||||||
return any(crawler in user_agent for crawler in CRAWLERS)
|
return any(crawler in user_agent for crawler in CRAWLERS)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@cache
|
@lru_cache(maxsize=128)
|
||||||
def isDev(host: str) -> bool:
|
def isDev(host: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if the host indicates a development environment.
|
Check if the host indicates a development environment.
|
||||||
@@ -126,7 +135,7 @@ def isDev(host: str) -> bool:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@cache
|
@lru_cache(maxsize=128)
|
||||||
def getHandshakeScript(host: str) -> str:
|
def getHandshakeScript(host: str) -> str:
|
||||||
"""
|
"""
|
||||||
Get the handshake script HTML snippet.
|
Get the handshake script HTML snippet.
|
||||||
@@ -141,7 +150,7 @@ def getHandshakeScript(host: str) -> str:
|
|||||||
return ""
|
return ""
|
||||||
return '<script src="https://nathan.woodburn/handshake.js" domain="nathan.woodburn" async></script><script src="https://nathan.woodburn/https.js" async></script>'
|
return '<script src="https://nathan.woodburn/handshake.js" domain="nathan.woodburn" async></script><script src="https://nathan.woodburn/https.js" async></script>'
|
||||||
|
|
||||||
@cache
|
@lru_cache(maxsize=64)
|
||||||
def getAddress(coin: str) -> str:
|
def getAddress(coin: str) -> str:
|
||||||
"""
|
"""
|
||||||
Get the wallet address for a cryptocurrency.
|
Get the wallet address for a cryptocurrency.
|
||||||
@@ -160,7 +169,7 @@ def getAddress(coin: str) -> str:
|
|||||||
return address
|
return address
|
||||||
|
|
||||||
|
|
||||||
@cache
|
@lru_cache(maxsize=256)
|
||||||
def getFilePath(name: str, path: str) -> Optional[str]:
|
def getFilePath(name: str, path: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Find a file in a directory tree.
|
Find a file in a directory tree.
|
||||||
@@ -202,7 +211,6 @@ def json_response(request: Request, message: Union[str, Dict] = "404 Not Found",
|
|||||||
"ip": getClientIP(request),
|
"ip": getClientIP(request),
|
||||||
}), code
|
}), code
|
||||||
|
|
||||||
|
|
||||||
def error_response(
|
def error_response(
|
||||||
request: Request,
|
request: Request,
|
||||||
message: str = "404 Not Found",
|
message: str = "404 Not Found",
|
||||||
@@ -221,7 +229,7 @@ def error_response(
|
|||||||
Returns:
|
Returns:
|
||||||
Union[Tuple[Dict, int], object]: The JSON or HTML response
|
Union[Tuple[Dict, int], object]: The JSON or HTML response
|
||||||
"""
|
"""
|
||||||
if force_json or isCurl(request):
|
if force_json or isCLI(request):
|
||||||
return json_response(request, message, code)
|
return json_response(request, message, code)
|
||||||
|
|
||||||
# Check if <error code>.html exists in templates
|
# Check if <error code>.html exists in templates
|
||||||
|
|||||||
876
uv.lock
generated
Normal file
876
uv.lock
generated
Normal file
@@ -0,0 +1,876 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotated-types"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ansi2html"
|
||||||
|
version = "1.9.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/4b/d5/e3546dcd5e4a9566f4ed8708df5853e83ca627461a5b048a861c6f8e7a26/ansi2html-1.9.2.tar.gz", hash = "sha256:3453bf87535d37b827b05245faaa756dbab4ec3d69925e352b6319c3c955c0a5", size = 44300, upload-time = "2024-06-22T17:33:23.964Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/71/aee71b836e9ee2741d5694b80d74bfc7c8cd5dbdf7a9f3035fcf80d792b1/ansi2html-1.9.2-py3-none-any.whl", hash = "sha256:dccb75aa95fb018e5d299be2b45f802952377abfdce0504c17a6ee6ef0a420c5", size = 17614, upload-time = "2024-06-22T17:33:21.852Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.11.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "sniffio" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "beautifulsoup4"
|
||||||
|
version = "4.14.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "soupsieve" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/77/e9/df2358efd7659577435e2177bfa69cba6c33216681af51a707193dec162a/beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e", size = 625822, upload-time = "2025-09-29T10:05:42.613Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392, upload-time = "2025-09-29T10:05:43.771Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blinker"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cachetools"
|
||||||
|
version = "6.2.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201", size = 31325, upload-time = "2025-10-12T14:55:30.139Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701", size = 11280, upload-time = "2025-10-12T14:55:28.382Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2025.10.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfgv"
|
||||||
|
version = "3.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "click"
|
||||||
|
version = "8.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cloudflare"
|
||||||
|
version = "4.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "distro" },
|
||||||
|
{ name = "httpx" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "sniffio" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5d/48/e481c0a9b9010a5c41b5ca78ff9fbe00dc8a9a4d39da5af610a4ec49c7f7/cloudflare-4.3.1.tar.gz", hash = "sha256:b1e1c6beeb8d98f63bfe0a1cba874fc4e22e000bcc490544f956c689b3b5b258", size = 1933187, upload-time = "2025-06-16T21:43:18.716Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/8f/c6c543565efd3144da4304efa5917aac06b6416a8663a6defe0e9b2b7569/cloudflare-4.3.1-py3-none-any.whl", hash = "sha256:6927135a5ee5633d6e2e1952ca0484745e933727aeeb189996d2ad9d292071c6", size = 4406465, upload-time = "2025-06-16T21:43:17.3Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "construct"
|
||||||
|
version = "2.10.68"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/a4a032e94bcfdff481f2e6fecd472794d9da09f474a2185ed33b2c7cad64/construct-2.10.68.tar.gz", hash = "sha256:7b2a3fd8e5f597a5aa1d614c3bd516fa065db01704c72a1efaaeec6ef23d8b45", size = 57856, upload-time = "2022-02-21T23:09:15.1Z" }
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "construct-typing"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "construct" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f1/13/c609e60a687252813aa4b69f989f42754ccd5e217717216fc852eefedfd7/construct-typing-0.6.2.tar.gz", hash = "sha256:948e998cfc003681dc34f2d071c3a688cf35b805cbe107febbc488ef967ccba1", size = 22029, upload-time = "2023-08-03T07:31:06.205Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/0b/ab3ce2b27dd74b6a6703065bd304ea8211ff4de3b1c304446ed95234177b/construct_typing-0.6.2-py3-none-any.whl", hash = "sha256:ebea6989ac622d0c4eb457092cef0c7bfbcfa110bd018670fea7064d0bc09e47", size = 23298, upload-time = "2023-08-03T07:31:04.545Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "distlib"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "distro"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filelock"
|
||||||
|
version = "3.20.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flask"
|
||||||
|
version = "3.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "blinker" },
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "itsdangerous" },
|
||||||
|
{ name = "jinja2" },
|
||||||
|
{ name = "markupsafe" },
|
||||||
|
{ name = "werkzeug" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flask-cors"
|
||||||
|
version = "6.0.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "flask" },
|
||||||
|
{ name = "werkzeug" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/76/37/bcfa6c7d5eec777c4c7cf45ce6b27631cebe5230caf88d85eadd63edd37a/flask_cors-6.0.1.tar.gz", hash = "sha256:d81bcb31f07b0985be7f48406247e9243aced229b7747219160a0559edd678db", size = 13463, upload-time = "2025-06-11T01:32:08.518Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/f8/01bf35a3afd734345528f98d0353f2a978a476528ad4d7e78b70c4d149dd/flask_cors-6.0.1-py3-none-any.whl", hash = "sha256:c7b2cbfb1a31aa0d2e5341eea03a6805349f7a61647daee1a15c46bbe981494c", size = 13244, upload-time = "2025-06-11T01:32:07.352Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gunicorn"
|
||||||
|
version = "23.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "packaging" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h11"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpcore"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "h11" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx"
|
||||||
|
version = "0.28.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "httpcore" },
|
||||||
|
{ name = "idna" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "identify"
|
||||||
|
version = "2.6.15"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "itsdangerous"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jinja2"
|
||||||
|
version = "3.1.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markupsafe" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonalias"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ec/45/ee7e17002cb7f3264f755ff6a1a72c55d1830e07808d643167d2a2277c4f/jsonalias-0.1.1.tar.gz", hash = "sha256:64f04d935397d579fc94509e1fcb6212f2d081235d9d6395bd10baedf760a769", size = 1095, upload-time = "2022-10-28T22:57:56.224Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/ed/05aebce69f78c104feff2ffcdd5a6f9d668a208aba3a8bf56e3750809fd8/jsonalias-0.1.1-py3-none-any.whl", hash = "sha256:a56d2888e6397812c606156504e861e8ec00e188005af149f003c787db3d3f18", size = 1312, upload-time = "2022-10-28T22:57:54.763Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markdown"
|
||||||
|
version = "3.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markupsafe"
|
||||||
|
version = "3.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nathanwoodburn-github-io"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "ansi2html" },
|
||||||
|
{ name = "beautifulsoup4" },
|
||||||
|
{ name = "cachetools" },
|
||||||
|
{ name = "cloudflare" },
|
||||||
|
{ name = "flask" },
|
||||||
|
{ name = "flask-cors" },
|
||||||
|
{ name = "gunicorn" },
|
||||||
|
{ name = "markdown" },
|
||||||
|
{ name = "pillow" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "qrcode" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "solana" },
|
||||||
|
{ name = "solders" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pre-commit" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "ansi2html", specifier = ">=1.9.2" },
|
||||||
|
{ name = "beautifulsoup4", specifier = ">=4.14.2" },
|
||||||
|
{ name = "cachetools", specifier = ">=6.2.1" },
|
||||||
|
{ name = "cloudflare", specifier = ">=4.3.1" },
|
||||||
|
{ name = "flask", specifier = ">=3.1.2" },
|
||||||
|
{ name = "flask-cors", specifier = ">=6.0.1" },
|
||||||
|
{ name = "gunicorn", specifier = ">=23.0.0" },
|
||||||
|
{ name = "markdown", specifier = ">=3.9" },
|
||||||
|
{ name = "pillow", specifier = ">=12.0.0" },
|
||||||
|
{ name = "pydantic", specifier = ">=2.12.3" },
|
||||||
|
{ name = "pygments", specifier = ">=2.19.2" },
|
||||||
|
{ name = "python-dateutil", specifier = ">=2.9.0.post0" },
|
||||||
|
{ name = "python-dotenv", specifier = ">=1.2.1" },
|
||||||
|
{ name = "qrcode", specifier = ">=8.2" },
|
||||||
|
{ name = "requests", specifier = ">=2.32.5" },
|
||||||
|
{ name = "solana", specifier = ">=0.36.9" },
|
||||||
|
{ name = "solders", specifier = ">=0.26.0" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [
|
||||||
|
{ name = "pre-commit", specifier = ">=4.4.0" },
|
||||||
|
{ name = "ruff", specifier = ">=0.14.5" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nodeenv"
|
||||||
|
version = "1.9.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "25.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pillow"
|
||||||
|
version = "12.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "4.5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pre-commit"
|
||||||
|
version = "4.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cfgv" },
|
||||||
|
{ name = "identify" },
|
||||||
|
{ name = "nodeenv" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "virtualenv" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a6/49/7845c2d7bf6474efd8e27905b51b11e6ce411708c91e829b93f324de9929/pre_commit-4.4.0.tar.gz", hash = "sha256:f0233ebab440e9f17cabbb558706eb173d19ace965c68cdce2c081042b4fab15", size = 197501, upload-time = "2025-11-08T21:12:11.607Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/11/574fe7d13acf30bfd0a8dd7fa1647040f2b8064f13f43e8c963b1e65093b/pre_commit-4.4.0-py2.py3-none-any.whl", hash = "sha256:b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813", size = 226049, upload-time = "2025-11-08T21:12:10.228Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic"
|
||||||
|
version = "2.12.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-types" },
|
||||||
|
{ name = "pydantic-core" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic-core"
|
||||||
|
version = "2.41.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.19.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dateutil"
|
||||||
|
version = "2.9.0.post0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dotenv"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "qrcode"
|
||||||
|
version = "8.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8f/b2/7fc2931bfae0af02d5f53b174e9cf701adbb35f39d69c2af63d4a39f81a9/qrcode-8.2.tar.gz", hash = "sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c", size = 43317, upload-time = "2025-05-01T15:44:24.726Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/b8/d2d6d731733f51684bbf76bf34dab3b70a9148e8f2cef2bb544fccec681a/qrcode-8.2-py3-none-any.whl", hash = "sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f", size = 45986, upload-time = "2025-05-01T15:44:22.781Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "ruff"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/82/fa/fbb67a5780ae0f704876cb8ac92d6d76da41da4dc72b7ed3565ab18f2f52/ruff-0.14.5.tar.gz", hash = "sha256:8d3b48d7d8aad423d3137af7ab6c8b1e38e4de104800f0d596990f6ada1a9fc1", size = 5615944, upload-time = "2025-11-13T19:58:51.155Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/31/c07e9c535248d10836a94e4f4e8c5a31a1beed6f169b31405b227872d4f4/ruff-0.14.5-py3-none-linux_armv6l.whl", hash = "sha256:f3b8248123b586de44a8018bcc9fefe31d23dda57a34e6f0e1e53bd51fd63594", size = 13171630, upload-time = "2025-11-13T19:57:54.894Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/5c/283c62516dca697cd604c2796d1487396b7a436b2f0ecc3fd412aca470e0/ruff-0.14.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f7a75236570318c7a30edd7f5491945f0169de738d945ca8784500b517163a72", size = 13413925, upload-time = "2025-11-13T19:57:59.181Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/f3/aa319f4afc22cb6fcba2b9cdfc0f03bbf747e59ab7a8c5e90173857a1361/ruff-0.14.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d146132d1ee115f8802356a2dc9a634dbf58184c51bff21f313e8cd1c74899a", size = 12574040, upload-time = "2025-11-13T19:58:02.056Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/7f/cb5845fcc7c7e88ed57f58670189fc2ff517fe2134c3821e77e29fd3b0c8/ruff-0.14.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2380596653dcd20b057794d55681571a257a42327da8894b93bbd6111aa801f", size = 13009755, upload-time = "2025-11-13T19:58:05.172Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/d2/bcbedbb6bcb9253085981730687ddc0cc7b2e18e8dc13cf4453de905d7a0/ruff-0.14.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68", size = 12937641, upload-time = "2025-11-13T19:58:08.345Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/58/e25de28a572bdd60ffc6bb71fc7fd25a94ec6a076942e372437649cbb02a/ruff-0.14.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88f0770d42b7fa02bbefddde15d235ca3aa24e2f0137388cc15b2dcbb1f7c7a7", size = 13610854, upload-time = "2025-11-13T19:58:11.419Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/24/43bb3fd23ecee9861970978ea1a7a63e12a204d319248a7e8af539984280/ruff-0.14.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78", size = 15061088, upload-time = "2025-11-13T19:58:14.551Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/44/a022f288d61c2f8c8645b24c364b719aee293ffc7d633a2ca4d116b9c716/ruff-0.14.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b595bedf6bc9cab647c4a173a61acf4f1ac5f2b545203ba82f30fcb10b0318fb", size = 14734717, upload-time = "2025-11-13T19:58:17.518Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/81/5c6ba44de7e44c91f68073e0658109d8373b0590940efe5bd7753a2585a3/ruff-0.14.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f55382725ad0bdb2e8ee2babcbbfb16f124f5a59496a2f6a46f1d9d99d93e6e2", size = 14028812, upload-time = "2025-11-13T19:58:20.533Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/ef/41a8b60f8462cb320f68615b00299ebb12660097c952c600c762078420f8/ruff-0.14.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7497d19dce23976bdaca24345ae131a1d38dcfe1b0850ad8e9e6e4fa321a6e19", size = 13825656, upload-time = "2025-11-13T19:58:23.345Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/00/207e5de737fdb59b39eb1fac806904fe05681981b46d6a6db9468501062e/ruff-0.14.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:410e781f1122d6be4f446981dd479470af86537fb0b8857f27a6e872f65a38e4", size = 13959922, upload-time = "2025-11-13T19:58:26.537Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/7e/fa1f5c2776db4be405040293618846a2dece5c70b050874c2d1f10f24776/ruff-0.14.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01be527ef4c91a6d55e53b337bfe2c0f82af024cc1a33c44792d6844e2331e1", size = 12932501, upload-time = "2025-11-13T19:58:29.822Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/d8/d86bf784d693a764b59479a6bbdc9515ae42c340a5dc5ab1dabef847bfaa/ruff-0.14.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f66e9bb762e68d66e48550b59c74314168ebb46199886c5c5aa0b0fbcc81b151", size = 12927319, upload-time = "2025-11-13T19:58:32.923Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/de/ee0b304d450ae007ce0cb3e455fe24fbcaaedae4ebaad6c23831c6663651/ruff-0.14.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d93be8f1fa01022337f1f8f3bcaa7ffee2d0b03f00922c45c2207954f351f465", size = 13206209, upload-time = "2025-11-13T19:58:35.952Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/aa/193ca7e3a92d74f17d9d5771a765965d2cf42c86e6f0fd95b13969115723/ruff-0.14.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c135d4b681f7401fe0e7312017e41aba9b3160861105726b76cfa14bc25aa367", size = 13953709, upload-time = "2025-11-13T19:58:39.002Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/f1/7119e42aa1d3bf036ffc9478885c2e248812b7de9abea4eae89163d2929d/ruff-0.14.5-py3-none-win32.whl", hash = "sha256:c83642e6fccfb6dea8b785eb9f456800dcd6a63f362238af5fc0c83d027dd08b", size = 12925808, upload-time = "2025-11-13T19:58:42.779Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/9d/7c0a255d21e0912114784e4a96bf62af0618e2190cae468cd82b13625ad2/ruff-0.14.5-py3-none-win_amd64.whl", hash = "sha256:9d55d7af7166f143c94eae1db3312f9ea8f95a4defef1979ed516dbb38c27621", size = 14331546, upload-time = "2025-11-13T19:58:45.691Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/80/69756670caedcf3b9be597a6e12276a6cf6197076eb62aad0c608f8efce0/ruff-0.14.5-py3-none-win_arm64.whl", hash = "sha256:4b700459d4649e2594b31f20a9de33bc7c19976d4746d8d0798ad959621d64a4", size = 13433331, upload-time = "2025-11-13T19:58:48.434Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sniffio"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "solana"
|
||||||
|
version = "0.36.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "construct-typing" },
|
||||||
|
{ name = "httpx" },
|
||||||
|
{ name = "solders" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "websockets" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8c/e0/ce762b6763e3a0f8a5ccecbf695d65ef54b6f874ad5f58ce5cdcaba224f1/solana-0.36.9.tar.gz", hash = "sha256:f702f6177337c67a982909ef54ef3abce5e795b8cd93edb045bedfa4d13c20c5", size = 52722, upload-time = "2025-08-09T16:23:25.307Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/11/d5e5d02200ca85b615da39078806b377156b67b2093c8bc08a1b9c293070/solana-0.36.9-py3-none-any.whl", hash = "sha256:e05824f91f95abe5a687914976e8bc78986386156f2106108c696db998c3c542", size = 62882, upload-time = "2025-08-09T16:23:24.149Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "solders"
|
||||||
|
version = "0.26.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "jsonalias" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/87/96/23ad2e43e2676b78834064fe051e3db3ce1899336ecd4797f92fcd06113a/solders-0.26.0.tar.gz", hash = "sha256:057533892d6fa432c1ce1e2f5e3428802964666c10b57d3d1bcaab86295f046c", size = 181123, upload-time = "2025-02-18T19:23:57.734Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/ce/58bbb4d2c696e770cdd37e5f6dc2891ef7610c0c085bf400f9c42dcff1ad/solders-0.26.0-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9c1a0ef5daa1a05934af5fb6e7e32eab7c42cede406c80067fee006f461ffc4a", size = 24344472, upload-time = "2025-02-18T19:23:30.273Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/35/221cec0e5900c2202833e7e9110c3405a2d96ed25e110b247f88b8782e29/solders-0.26.0-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b964efbd7c0b38aef3bf4293ea5938517ae649b9a23e7cd147d889931775aab", size = 6674734, upload-time = "2025-02-18T19:23:35.15Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/33/d17b7dbc92672351d59fc65cdb93b8924fc682deba09f6d96f25440187ae/solders-0.26.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36e6a769c5298b887b7588edb171d93709a89302aef75913fe893d11c653739d", size = 13472961, upload-time = "2025-02-18T19:23:38.582Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/e7/533367d815ab000587ccc37d89e154132f63347f02dcaaac5df72bd851de/solders-0.26.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b3cc55b971ec6ed1b4466fa7e7e09eee9baba492b8cd9e3204e3e1a0c5a0c4aa", size = 6886198, upload-time = "2025-02-18T19:23:41.453Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/e0/ab41ab3df5fdf3b0e55613be93a43c2fe58b15a6ea8ceca26d3fba02e3c6/solders-0.26.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3e3973074c17265921c70246a17bcf80972c5b96a3e1ed7f5049101f11865092", size = 7319170, upload-time = "2025-02-18T19:23:43.758Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/34/5174ce592607e0ac020aff203217f2f113a55eec49af3db12945fea42d89/solders-0.26.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:59b52419452602f697e659199a25acacda8365971c376ef3c0687aecdd929e07", size = 7134977, upload-time = "2025-02-18T19:23:46.157Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/5e/822faabda0d473c29bdf59fe8869a411fd436af8ca6f5d6e89f7513f682f/solders-0.26.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5946ec3f2a340afa9ce5c2b8ab628ae1dea2ad2235551b1297cafdd7e3e5c51a", size = 6984222, upload-time = "2025-02-18T19:23:49.429Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/e8/dc992f677762ea2de44b7768120d95887ef39fab10d6f29fb53e6a9882c1/solders-0.26.0-cp37-abi3-win_amd64.whl", hash = "sha256:5466616610170aab08c627ae01724e425bcf90085bc574da682e9f3bd954900b", size = 5480492, upload-time = "2025-02-18T19:23:53.285Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "soupsieve"
|
||||||
|
version = "2.8"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-inspection"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "virtualenv"
|
||||||
|
version = "20.35.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "distlib" },
|
||||||
|
{ name = "filelock" },
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "websockets"
|
||||||
|
version = "15.0.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "werkzeug"
|
||||||
|
version = "3.1.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markupsafe" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user