Compare commits
2 Commits
main
...
20863d024a
| Author | SHA1 | Date | |
|---|---|---|---|
|
20863d024a
|
|||
|
1965ff762c
|
@@ -1,33 +1,19 @@
|
|||||||
# Bytecode and virtualenvs
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
|
||||||
*.pyo
|
|
||||||
.venv/
|
|
||||||
.vscode/
|
|
||||||
.vs/
|
|
||||||
.ruff_check/
|
|
||||||
.env
|
.env
|
||||||
|
.vs/
|
||||||
# Pycache in subdirectories
|
.venv/
|
||||||
**/__pycache__/
|
*.tmp
|
||||||
**/*.pyc
|
|
||||||
**/*.pyo
|
|
||||||
|
|
||||||
# Git and CI
|
|
||||||
.git/
|
|
||||||
.gitea/
|
|
||||||
testing/
|
testing/
|
||||||
tests/
|
tests/
|
||||||
|
.vscode/
|
||||||
|
.ruff_check/
|
||||||
|
.gitea/
|
||||||
|
|
||||||
# Build and docs
|
|
||||||
Dockerfile
|
# Random files
|
||||||
NathanWoodburn.bsdesign
|
|
||||||
LICENSE.txt
|
|
||||||
README.md
|
README.md
|
||||||
|
LICENSE.txt
|
||||||
|
NathanWoodburn.bsdesign
|
||||||
# Development caches
|
Dockerfile
|
||||||
*.tmp
|
|
||||||
*.log
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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
|
|
||||||
74
Dockerfile
74
Dockerfile
@@ -1,63 +1,27 @@
|
|||||||
# syntax=docker/dockerfile:1
|
FROM --platform=$BUILDPLATFORM python:3.13-alpine
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||||
|
|
||||||
### Build stage ###
|
# Install curl for healthcheck
|
||||||
FROM python:3.13-alpine AS build
|
RUN apk add --no-cache curl
|
||||||
|
|
||||||
# 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/
|
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY pyproject.toml uv.lock ./
|
|
||||||
|
|
||||||
# Install dependencies into a virtual environment
|
# Install dependencies
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
|
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||||
|
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||||
|
uv sync --locked --no-install-project
|
||||||
|
|
||||||
|
# Copy the project into the image
|
||||||
|
ADD . /app
|
||||||
|
|
||||||
|
# Sync the project
|
||||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
uv sync --locked
|
uv sync --locked
|
||||||
|
|
||||||
# Copy only app source files
|
# Add mount point for data volume
|
||||||
COPY blueprints blueprints
|
# VOLUME /data
|
||||||
COPY main.py server.py curl.py tools.py mail.py cache_helper.py ./
|
|
||||||
COPY templates templates
|
|
||||||
COPY data data
|
|
||||||
COPY pwa pwa
|
|
||||||
COPY .well-known .well-known
|
|
||||||
|
|
||||||
# Clean up caches and pycache
|
ENTRYPOINT ["uv", "run"]
|
||||||
RUN rm -rf /root/.cache/uv
|
CMD ["main.py"]
|
||||||
RUN find . -type d -name "__pycache__" -exec rm -rf {} +
|
|
||||||
|
|
||||||
|
|
||||||
### 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/
|
|
||||||
COPY --from=build --chown=appuser:appgroup /app/cache_helper.py /app/
|
|
||||||
|
|
||||||
USER appuser
|
|
||||||
EXPOSE 5000
|
|
||||||
|
|
||||||
ENTRYPOINT ["python3", "main.py"]
|
|
||||||
|
|||||||
Binary file not shown.
35
addCoin.py
35
addCoin.py
@@ -1,38 +1,35 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
if not os.path.exists(".well-known/wallets"):
|
if not os.path.exists('.well-known/wallets'):
|
||||||
os.makedirs(".well-known/wallets")
|
os.makedirs('.well-known/wallets')
|
||||||
|
|
||||||
|
def addCoin(token:str, name:str, address:str):
|
||||||
def addCoin(token: str, name: str, address: str):
|
with open('.well-known/wallets/'+token.upper(),'w') as f:
|
||||||
with open(".well-known/wallets/" + token.upper(), "w") as f:
|
|
||||||
f.write(address)
|
f.write(address)
|
||||||
|
|
||||||
with open(".well-known/wallets/.coins", "r") as f:
|
with open('.well-known/wallets/.coins','r') as f:
|
||||||
coins = json.load(f)
|
coins = json.load(f)
|
||||||
|
|
||||||
coins[token.upper()] = f"{name} ({token.upper()})"
|
coins[token.upper()] = f'{name} ({token.upper()})'
|
||||||
with open(".well-known/wallets/.coins", "w") as f:
|
with open('.well-known/wallets/.coins','w') as f:
|
||||||
f.write(json.dumps(coins, indent=4))
|
f.write(json.dumps(coins, indent=4))
|
||||||
|
|
||||||
|
def addDomain(token:str, domain:str):
|
||||||
def addDomain(token: str, domain: str):
|
with open('.well-known/wallets/.domains','r') as f:
|
||||||
with open(".well-known/wallets/.domains", "r") as f:
|
|
||||||
domains = json.load(f)
|
domains = json.load(f)
|
||||||
|
|
||||||
domains[token.upper()] = domain
|
domains[token.upper()] = domain
|
||||||
with open(".well-known/wallets/.domains", "w") as f:
|
with open('.well-known/wallets/.domains','w') as f:
|
||||||
f.write(json.dumps(domains, indent=4))
|
f.write(json.dumps(domains, indent=4))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
if __name__ == "__main__":
|
|
||||||
# Ask user for token
|
# Ask user for token
|
||||||
token = input("Enter token symbol: ")
|
token = input('Enter token symbol: ')
|
||||||
name = input("Enter token name: ")
|
name = input('Enter token name: ')
|
||||||
address = input("Enter wallet address: ")
|
address = input('Enter wallet address: ')
|
||||||
addCoin(token, name, address)
|
addCoin(token, name, address)
|
||||||
|
|
||||||
if input("Do you want to add a domain? (y/n): ").lower() == "y":
|
if input('Do you want to add a domain? (y/n): ').lower() == 'y':
|
||||||
domain = input("Enter domain: ")
|
domain = input('Enter domain: ')
|
||||||
addDomain(token, domain)
|
addDomain(token, domain)
|
||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
from cloudflare import Cloudflare
|
from cloudflare import Cloudflare
|
||||||
from tools import json_response
|
from tools import json_response
|
||||||
|
|
||||||
app = Blueprint("acme", __name__)
|
app = Blueprint('acme', __name__)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/hnsdoh-acme", methods=["POST"])
|
@app.route("/hnsdoh-acme", methods=["POST"])
|
||||||
@@ -23,9 +23,7 @@ def post():
|
|||||||
zone = cf.zones.list(name="hnsdoh.com").to_dict()
|
zone = cf.zones.list(name="hnsdoh.com").to_dict()
|
||||||
zone_id = zone["result"][0]["id"] # type: ignore
|
zone_id = zone["result"][0]["id"] # type: ignore
|
||||||
existing_records = cf.dns.records.list(
|
existing_records = cf.dns.records.list(
|
||||||
zone_id=zone_id,
|
zone_id=zone_id, type="TXT", name="_acme-challenge.hnsdoh.com" # type: ignore
|
||||||
type="TXT",
|
|
||||||
name="_acme-challenge.hnsdoh.com", # type: ignore
|
|
||||||
).to_dict()
|
).to_dict()
|
||||||
record_id = existing_records["result"][0]["id"] # type: ignore
|
record_id = existing_records["result"][0]["id"] # type: ignore
|
||||||
cf.dns.records.delete(dns_record_id=record_id, zone_id=zone_id)
|
cf.dns.records.delete(dns_record_id=record_id, zone_id=zone_id)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from tools import getClientIP, getGitCommit, json_response, parse_date, get_tool
|
|||||||
from blueprints import sol
|
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
|
||||||
@@ -18,17 +17,24 @@ HTTP_NOT_FOUND = 404
|
|||||||
HTTP_UNSUPPORTED_MEDIA = 415
|
HTTP_UNSUPPORTED_MEDIA = 415
|
||||||
HTTP_SERVER_ERROR = 500
|
HTTP_SERVER_ERROR = 500
|
||||||
|
|
||||||
app = Blueprint("api", __name__, url_prefix="/api/v1")
|
app = Blueprint('api', __name__, url_prefix='/api/v1')
|
||||||
# Register solana blueprint
|
# Register solana blueprint
|
||||||
app.register_blueprint(sol.app)
|
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
|
||||||
|
|
||||||
|
|
||||||
@app.route("/", strict_slashes=False)
|
@app.route("/", strict_slashes=False)
|
||||||
@app.route("/help")
|
@app.route("/help")
|
||||||
def help():
|
def help():
|
||||||
"""Provide API documentation and help."""
|
"""Provide API documentation and help."""
|
||||||
return jsonify(
|
return jsonify({
|
||||||
{
|
|
||||||
"message": "Welcome to Nathan.Woodburn/ API! This is a personal website. For more information, visit https://nathan.woodburn.au",
|
"message": "Welcome to Nathan.Woodburn/ API! This is a personal website. For more information, visit https://nathan.woodburn.au",
|
||||||
"endpoints": {
|
"endpoints": {
|
||||||
"/time": "Get the current time",
|
"/time": "Get the current time",
|
||||||
@@ -43,22 +49,19 @@ def help():
|
|||||||
"/ping": "Just check if the site is up",
|
"/ping": "Just check if the site is up",
|
||||||
"/ip": "Get your IP address",
|
"/ip": "Get your IP address",
|
||||||
"/headers": "Get your request headers",
|
"/headers": "Get your request headers",
|
||||||
"/help": "Get this help message",
|
"/help": "Get this help message"
|
||||||
},
|
},
|
||||||
"base_url": "/api/v1",
|
"base_url": "/api/v1",
|
||||||
"version": getGitCommit(),
|
"version": getGitCommit(),
|
||||||
"ip": getClientIP(request),
|
"ip": getClientIP(request),
|
||||||
"status": HTTP_OK,
|
"status": HTTP_OK
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/status")
|
@app.route("/status")
|
||||||
@app.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)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/version")
|
@app.route("/version")
|
||||||
def version():
|
def version():
|
||||||
"""Get the current version of the website."""
|
"""Get the current version of the website."""
|
||||||
@@ -68,48 +71,46 @@ def version():
|
|||||||
@app.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."""
|
||||||
nc_config = get_nc_config()
|
timezone_offset = datetime.timedelta(hours=NC_CONFIG["time-zone"])
|
||||||
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
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.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
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.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), "status": HTTP_OK}
|
"ip": getClientIP(request),
|
||||||
)
|
"status": HTTP_OK
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/ip")
|
@app.route("/ip")
|
||||||
def ip():
|
def ip():
|
||||||
"""Get the client's IP address."""
|
"""Get the client's IP address."""
|
||||||
return jsonify({"ip": getClientIP(request), "status": HTTP_OK})
|
return jsonify({
|
||||||
|
"ip": getClientIP(request),
|
||||||
|
"status": HTTP_OK
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/email", methods=["POST"])
|
@app.route("/email", methods=["POST"])
|
||||||
@@ -117,9 +118,7 @@ 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
|
||||||
if not request.is_json:
|
if not request.is_json:
|
||||||
return json_response(
|
return json_response(request, "415 Unsupported Media Type", HTTP_UNSUPPORTED_MEDIA)
|
||||||
request, "415 Unsupported Media Type", HTTP_UNSUPPORTED_MEDIA
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if api key sent
|
# Check if api key sent
|
||||||
data = request.json
|
data = request.json
|
||||||
@@ -139,27 +138,35 @@ def email_post():
|
|||||||
@app.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 = {
|
||||||
"name": repo_name,
|
"website": None,
|
||||||
"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,
|
||||||
"repo_description": repo_description,
|
"repo_description": repo_description,
|
||||||
"repo": gitinfo,
|
"repo": gitinfo,
|
||||||
"ip": getClientIP(request),
|
"ip": getClientIP(request),
|
||||||
"status": HTTP_OK,
|
"status": HTTP_OK
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/tools")
|
@app.route("/tools")
|
||||||
def tools():
|
def tools():
|
||||||
@@ -172,7 +179,6 @@ def tools():
|
|||||||
|
|
||||||
return json_response(request, {"tools": tools}, HTTP_OK)
|
return json_response(request, {"tools": tools}, HTTP_OK)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/playing")
|
@app.route("/playing")
|
||||||
def playing():
|
def playing():
|
||||||
"""Get the currently playing Spotify track."""
|
"""Get the currently playing Spotify track."""
|
||||||
@@ -195,11 +201,15 @@ def headers():
|
|||||||
# Remove from headers
|
# Remove from headers
|
||||||
toremove.append(key)
|
toremove.append(key)
|
||||||
|
|
||||||
|
|
||||||
for key in toremove:
|
for key in toremove:
|
||||||
headers.pop(key)
|
headers.pop(key)
|
||||||
|
|
||||||
return jsonify({"headers": headers, "ip": getClientIP(request), "status": HTTP_OK})
|
return jsonify({
|
||||||
|
"headers": headers,
|
||||||
|
"ip": getClientIP(request),
|
||||||
|
"status": HTTP_OK
|
||||||
|
})
|
||||||
|
|
||||||
@app.route("/page_date")
|
@app.route("/page_date")
|
||||||
def page_date():
|
def page_date():
|
||||||
@@ -216,33 +226,33 @@ def page_date():
|
|||||||
r = requests.get(url, timeout=5)
|
r = requests.get(url, timeout=5)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
return json_response(
|
return json_response(request, f"400 Bad Request 'url' unreachable: {e}", HTTP_BAD_REQUEST)
|
||||||
request, f"400 Bad Request 'url' unreachable: {e}", HTTP_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
page_text = r.text
|
page_text = r.text
|
||||||
|
|
||||||
# Remove ordinal suffixes globally
|
# Remove ordinal suffixes globally
|
||||||
page_text = re.sub(r"(\d+)(st|nd|rd|th)", r"\1", page_text, flags=re.IGNORECASE)
|
page_text = re.sub(r'(\d+)(st|nd|rd|th)', r'\1', page_text, flags=re.IGNORECASE)
|
||||||
# Remove HTML comments
|
# Remove HTML comments
|
||||||
page_text = re.sub(r"<!--.*?-->", "", page_text, flags=re.DOTALL)
|
page_text = re.sub(r'<!--.*?-->', '', page_text, flags=re.DOTALL)
|
||||||
|
|
||||||
date_patterns = [
|
date_patterns = [
|
||||||
r"(\d{4})[/-](\d{1,2})[/-](\d{1,2})", # YYYY-MM-DD
|
r'(\d{4})[/-](\d{1,2})[/-](\d{1,2})', # YYYY-MM-DD
|
||||||
r"(\d{1,2})[/-](\d{1,2})[/-](\d{4})", # DD-MM-YYYY
|
r'(\d{1,2})[/-](\d{1,2})[/-](\d{4})', # DD-MM-YYYY
|
||||||
r"(?:Last updated:|Updated:|Updated last:)?\s*(\d{1,2})\s+([A-Za-z]{3,9})[, ]?\s*(\d{4})", # DD Month YYYY
|
r'(?:Last updated:|Updated:|Updated last:)?\s*(\d{1,2})\s+([A-Za-z]{3,9})[, ]?\s*(\d{4})', # DD Month YYYY
|
||||||
r"(?:\b\w+\b\s+){0,3}([A-Za-z]{3,9})\s+(\d{1,2}),?\s*(\d{4})", # Month DD, YYYY with optional words
|
r'(?:\b\w+\b\s+){0,3}([A-Za-z]{3,9})\s+(\d{1,2}),?\s*(\d{4})', # Month DD, YYYY with optional words
|
||||||
r"\b(\d{4})(\d{2})(\d{2})\b", # YYYYMMDD
|
r'\b(\d{4})(\d{2})(\d{2})\b', # YYYYMMDD
|
||||||
r"(?:Last updated:|Updated:|Last update)?\s*([A-Za-z]{3,9})\s+(\d{4})", # Month YYYY only
|
r'(?:Last updated:|Updated:|Last update)?\s*([A-Za-z]{3,9})\s+(\d{4})', # Month YYYY only
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Structured data patterns
|
# Structured data patterns
|
||||||
json_date_patterns = {
|
json_date_patterns = {
|
||||||
r'"datePublished"\s*:\s*"([^"]+)"': "published",
|
r'"datePublished"\s*:\s*"([^"]+)"': "published",
|
||||||
r'"dateModified"\s*:\s*"([^"]+)"': "modified",
|
r'"dateModified"\s*:\s*"([^"]+)"': "modified",
|
||||||
r'<meta\s+(?:[^>]*?)property\s*=\s*"article:published_time"\s+content\s*=\s*"([^"]+)"': "published",
|
r'<meta\s+(?:[^>]*?)property\s*=\s*"article:published_time"\s+content\s*=\s*"([^"]+)"': "published",
|
||||||
r'<meta\s+(?:[^>]*?)property\s*=\s*"article:modified_time"\s+content\s*=\s*"([^"]+)"': "modified",
|
r'<meta\s+(?:[^>]*?)property\s*=\s*"article:modified_time"\s+content\s*=\s*"([^"]+)"': "modified",
|
||||||
r'<time\s+datetime\s*=\s*"([^"]+)"': "published",
|
r'<time\s+datetime\s*=\s*"([^"]+)"': "published"
|
||||||
}
|
}
|
||||||
|
|
||||||
found_dates = []
|
found_dates = []
|
||||||
@@ -260,7 +270,7 @@ def page_date():
|
|||||||
for match in re.findall(pattern, page_text):
|
for match in re.findall(pattern, page_text):
|
||||||
try:
|
try:
|
||||||
dt = date_parser.isoparse(match)
|
dt = date_parser.isoparse(match)
|
||||||
formatted_date = dt.strftime("%Y-%m-%d")
|
formatted_date = dt.strftime('%Y-%m-%d')
|
||||||
found_dates.append([[formatted_date], -1, date_type])
|
found_dates.append([[formatted_date], -1, date_type])
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
continue
|
continue
|
||||||
@@ -269,9 +279,7 @@ def page_date():
|
|||||||
return json_response(request, "Date not found on page", HTTP_BAD_REQUEST)
|
return json_response(request, "Date not found on page", HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
today = datetime.date.today()
|
today = datetime.date.today()
|
||||||
tolerance_date = today + datetime.timedelta(
|
tolerance_date = today + datetime.timedelta(days=1) # Allow for slight future dates (e.g., time zones)
|
||||||
days=1
|
|
||||||
) # Allow for slight future dates (e.g., time zones)
|
|
||||||
# When processing dates
|
# When processing dates
|
||||||
processed_dates = []
|
processed_dates = []
|
||||||
for date_groups, pattern_format, date_type in found_dates:
|
for date_groups, pattern_format, date_type in found_dates:
|
||||||
@@ -292,32 +300,18 @@ def page_date():
|
|||||||
date_obj = {"date": dt.strftime("%Y-%m-%d"), "type": date_type}
|
date_obj = {"date": dt.strftime("%Y-%m-%d"), "type": date_type}
|
||||||
if verbose:
|
if verbose:
|
||||||
if pattern_format == -1:
|
if pattern_format == -1:
|
||||||
date_obj.update(
|
date_obj.update({"source": "metadata", "pattern_used": pattern_format, "raw": date_groups[0]})
|
||||||
{
|
|
||||||
"source": "metadata",
|
|
||||||
"pattern_used": pattern_format,
|
|
||||||
"raw": date_groups[0],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
date_obj.update(
|
date_obj.update({"source": "content", "pattern_used": pattern_format, "raw": " ".join(date_groups)})
|
||||||
{
|
|
||||||
"source": "content",
|
|
||||||
"pattern_used": pattern_format,
|
|
||||||
"raw": " ".join(date_groups),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
processed_dates.append(date_obj)
|
processed_dates.append(date_obj)
|
||||||
|
|
||||||
if not processed_dates:
|
if not processed_dates:
|
||||||
if verbose:
|
if verbose:
|
||||||
return jsonify(
|
return jsonify({
|
||||||
{
|
|
||||||
"message": "No valid dates found on page",
|
"message": "No valid dates found on page",
|
||||||
"found_dates": found_dates,
|
"found_dates": found_dates,
|
||||||
"processed_dates": processed_dates,
|
"processed_dates": processed_dates
|
||||||
}
|
}), HTTP_BAD_REQUEST
|
||||||
), HTTP_BAD_REQUEST
|
|
||||||
return json_response(request, "No valid dates found on page", HTTP_BAD_REQUEST)
|
return json_response(request, "No valid dates found on page", HTTP_BAD_REQUEST)
|
||||||
# Sort dates and return latest
|
# Sort dates and return latest
|
||||||
processed_dates.sort(key=lambda x: x["date"])
|
processed_dates.sort(key=lambda x: x["date"])
|
||||||
|
|||||||
@@ -3,83 +3,63 @@ 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 functools import lru_cache
|
|
||||||
from tools import isCLI, getClientIP, getHandshakeScript
|
from tools import isCLI, getClientIP, getHandshakeScript
|
||||||
|
|
||||||
app = Blueprint("blog", __name__, url_prefix="/blog")
|
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
|
||||||
blog_pages.sort(
|
blog_pages.sort(
|
||||||
key=lambda x: os.path.getmtime(os.path.join("data/blog", x)), reverse=True
|
key=lambda x: os.path.getmtime(os.path.join("data/blog", x)), reverse=True)
|
||||||
)
|
|
||||||
|
|
||||||
# Remove .md extension
|
# Remove .md extension
|
||||||
blog_pages = [
|
blog_pages = [page.removesuffix(".md")
|
||||||
page.removesuffix(".md") for page in blog_pages if page.endswith(".md")
|
for page in blog_pages if page.endswith(".md")]
|
||||||
]
|
|
||||||
|
|
||||||
return blog_pages
|
return blog_pages
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=64)
|
|
||||||
def get_blog_content(date):
|
|
||||||
"""Get and cache blog content."""
|
|
||||||
if not os.path.exists(f"data/blog/{date}.md"):
|
|
||||||
return None
|
|
||||||
|
|
||||||
with open(f"data/blog/{date}.md", "r") as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=64)
|
|
||||||
def render_markdown_to_html(content):
|
|
||||||
"""Convert markdown to HTML with caching."""
|
|
||||||
html = markdown.markdown(
|
|
||||||
content, extensions=["sane_lists", "codehilite", "fenced_code"]
|
|
||||||
)
|
|
||||||
# Add target="_blank" to all links
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def render_page(date, handshake_scripts=None):
|
def render_page(date, handshake_scripts=None):
|
||||||
# Get cached content
|
# Convert md to html
|
||||||
content = get_blog_content(date)
|
if not os.path.exists(f"data/blog/{date}.md"):
|
||||||
if content is None:
|
|
||||||
return render_template("404.html"), 404
|
return render_template("404.html"), 404
|
||||||
|
|
||||||
|
with open(f"data/blog/{date}.md", "r") as f:
|
||||||
|
content = f.read()
|
||||||
# Get the title from the file name
|
# Get the title from the file name
|
||||||
title = date.removesuffix(".md").replace("_", " ")
|
title = date.removesuffix(".md").replace("_", " ")
|
||||||
# Convert the md to html (cached)
|
# Convert the md to html
|
||||||
html_content = render_markdown_to_html(content)
|
content = markdown.markdown(
|
||||||
|
content, extensions=['sane_lists', 'codehilite', 'fenced_code'])
|
||||||
|
# Add target="_blank" to all links
|
||||||
|
content = content.replace('<a href="', '<a target="_blank" href="')
|
||||||
|
|
||||||
|
content = content.replace("<h4", "<h4 style='margin-bottom:0px;'")
|
||||||
|
content = fix_numbered_lists(content)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"blog/template.html",
|
"blog/template.html",
|
||||||
title=title,
|
title=title,
|
||||||
content=html_content,
|
content=content,
|
||||||
handshake_scripts=handshake_scripts,
|
handshake_scripts=handshake_scripts,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def fix_numbered_lists(html):
|
def fix_numbered_lists(html):
|
||||||
soup = BeautifulSoup(html, "html.parser")
|
soup = BeautifulSoup(html, 'html.parser')
|
||||||
|
|
||||||
# Find the <p> tag containing numbered steps
|
# Find the <p> tag containing numbered steps
|
||||||
paragraphs = soup.find_all("p")
|
paragraphs = soup.find_all('p')
|
||||||
for p in paragraphs:
|
for p in paragraphs:
|
||||||
content = p.decode_contents() # type: ignore
|
content = p.decode_contents() # type: ignore
|
||||||
|
|
||||||
# Check for likely numbered step structure
|
# Check for likely numbered step structure
|
||||||
if re.search(r"1\.\s", content):
|
if re.search(r'1\.\s', content):
|
||||||
# Split into pre-list and numbered steps
|
# Split into pre-list and numbered steps
|
||||||
# Match: <br>, optional whitespace, then a number and dot
|
# Match: <br>, optional whitespace, then a number and dot
|
||||||
parts = re.split(r"(?:<br\s*/?>)?\s*(\d+)\.\s", content)
|
parts = re.split(r'(?:<br\s*/?>)?\s*(\d+)\.\s', content)
|
||||||
|
|
||||||
# Result: [pre-text, '1', step1, '2', step2, ..., '10', step10]
|
# Result: [pre-text, '1', step1, '2', step2, ..., '10', step10]
|
||||||
pre_text = parts[0].strip()
|
pre_text = parts[0].strip()
|
||||||
@@ -88,9 +68,10 @@ def fix_numbered_lists(html):
|
|||||||
# Assemble the ordered list
|
# Assemble the ordered list
|
||||||
ol_items = []
|
ol_items = []
|
||||||
for i in range(0, len(steps), 2):
|
for i in range(0, len(steps), 2):
|
||||||
if i + 1 < len(steps):
|
if i+1 < len(steps):
|
||||||
step_html = steps[i + 1].strip()
|
step_html = steps[i+1].strip()
|
||||||
ol_items.append(f"<li style='list-style: auto;'>{step_html}</li>")
|
ol_items.append(
|
||||||
|
f"<li style='list-style: auto;'>{step_html}</li>")
|
||||||
|
|
||||||
# Build the final list HTML
|
# Build the final list HTML
|
||||||
ol_html = "<ol>\n" + "\n".join(ol_items) + "\n</ol>"
|
ol_html = "<ol>\n" + "\n".join(ol_items) + "\n</ol>"
|
||||||
@@ -99,7 +80,7 @@ def fix_numbered_lists(html):
|
|||||||
new_html = f"{pre_text}<br />\n{ol_html}" if pre_text else ol_html
|
new_html = f"{pre_text}<br />\n{ol_html}" if pre_text else ol_html
|
||||||
|
|
||||||
# Replace old <p> with parsed version
|
# Replace old <p> with parsed version
|
||||||
new_fragment = BeautifulSoup(new_html, "html.parser")
|
new_fragment = BeautifulSoup(new_html, 'html.parser')
|
||||||
p.replace_with(new_fragment)
|
p.replace_with(new_fragment)
|
||||||
break # Only process the first matching <p>
|
break # Only process the first matching <p>
|
||||||
|
|
||||||
@@ -136,23 +117,16 @@ def index():
|
|||||||
blog_pages = list_page_files()
|
blog_pages = list_page_files()
|
||||||
# Create a html list of pages
|
# Create a html list of pages
|
||||||
blog_pages = [
|
blog_pages = [
|
||||||
{
|
{"name": page.replace("_", " "), "url": f"/blog/{page}", "download": f"/blog/{page}.md"} for page in blog_pages
|
||||||
"name": page.replace("_", " "),
|
|
||||||
"url": f"/blog/{page}",
|
|
||||||
"download": f"/blog/{page}.md",
|
|
||||||
}
|
|
||||||
for page in blog_pages
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Render the template
|
# Render the template
|
||||||
return jsonify(
|
return jsonify({
|
||||||
{
|
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Check out my various blog postsa",
|
"message": "Check out my various blog postsa",
|
||||||
"ip": getClientIP(request),
|
"ip": getClientIP(request),
|
||||||
"blogs": blog_pages,
|
"blogs": blog_pages
|
||||||
}
|
}), 200
|
||||||
), 200
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
@@ -160,30 +134,31 @@ def path(path):
|
|||||||
if not isCLI(request):
|
if not isCLI(request):
|
||||||
return render_page(path, handshake_scripts=getHandshakeScript(request.host))
|
return render_page(path, handshake_scripts=getHandshakeScript(request.host))
|
||||||
|
|
||||||
# Get cached content
|
# Convert md to html
|
||||||
content = get_blog_content(path)
|
if not os.path.exists(f"data/blog/{path}.md"):
|
||||||
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({
|
||||||
{
|
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": f"Blog post: {title}",
|
"message": f"Blog post: {title}",
|
||||||
"ip": getClientIP(request),
|
"ip": getClientIP(request),
|
||||||
"title": title,
|
"title": title,
|
||||||
"content": content,
|
"content": content,
|
||||||
"download": f"/blog/{path}.md",
|
"download": f"/blog/{path}.md"
|
||||||
}
|
}), 200
|
||||||
), 200
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<path:path>.md")
|
@app.route("/<path:path>.md")
|
||||||
def path_md(path):
|
def path_md(path):
|
||||||
content = get_blog_content(path)
|
if not os.path.exists(f"data/blog/{path}.md"):
|
||||||
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,17 +1,15 @@
|
|||||||
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 functools import lru_cache
|
|
||||||
from tools import getHandshakeScript, error_response, isCLI
|
from tools import getHandshakeScript, error_response, isCLI
|
||||||
from curl import get_header, MAX_WIDTH
|
from curl import get_header, MAX_WIDTH
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
import re
|
import re
|
||||||
|
|
||||||
# Create blueprint
|
# Create blueprint
|
||||||
app = Blueprint("now", __name__, url_prefix="/now")
|
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 = [
|
||||||
@@ -21,14 +19,12 @@ 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]
|
||||||
@@ -55,10 +51,7 @@ def render(date, handshake_scripts=None):
|
|||||||
|
|
||||||
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(
|
return render_template(f"now/{date}.html", DATE=date_formatted, handshake_scripts=handshake_scripts)
|
||||||
f"now/{date}.html", DATE=date_formatted, handshake_scripts=handshake_scripts
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def render_curl(date=None):
|
def render_curl(date=None):
|
||||||
# If the date is not available, render the latest page
|
# If the date is not available, render the latest page
|
||||||
@@ -78,7 +71,7 @@ def render_curl(date=None):
|
|||||||
# Load HTML
|
# Load HTML
|
||||||
with open(f"templates/now/{date}.html", "r", encoding="utf-8") as f:
|
with open(f"templates/now/{date}.html", "r", encoding="utf-8") as f:
|
||||||
raw_html = f.read().replace("{{ date }}", date_formatted)
|
raw_html = f.read().replace("{{ date }}", date_formatted)
|
||||||
soup = BeautifulSoup(raw_html, "html.parser")
|
soup = BeautifulSoup(raw_html, 'html.parser')
|
||||||
|
|
||||||
posts = []
|
posts = []
|
||||||
|
|
||||||
@@ -110,7 +103,7 @@ def render_curl(date=None):
|
|||||||
for line in text.splitlines():
|
for line in text.splitlines():
|
||||||
while len(line) > MAX_WIDTH:
|
while len(line) > MAX_WIDTH:
|
||||||
# Find last space within max_width
|
# Find last space within max_width
|
||||||
split_at = line.rfind(" ", 0, MAX_WIDTH)
|
split_at = line.rfind(' ', 0, MAX_WIDTH)
|
||||||
if split_at == -1:
|
if split_at == -1:
|
||||||
split_at = MAX_WIDTH
|
split_at = MAX_WIDTH
|
||||||
wrapped_lines.append(line[:split_at].rstrip())
|
wrapped_lines.append(line[:split_at].rstrip())
|
||||||
@@ -131,9 +124,8 @@ def render_curl(date=None):
|
|||||||
for post in posts:
|
for post in posts:
|
||||||
response += f"[1m{post['header']}[0m\n\n{post['content']}\n\n"
|
response += f"[1m{post['header']}[0m\n\n{post['content']}\n\n"
|
||||||
|
|
||||||
return render_template(
|
return render_template("now.ascii", date=date_formatted, content=response, header=get_header())
|
||||||
"now.ascii", date=date_formatted, content=response, header=get_header()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/", strict_slashes=False)
|
@app.route("/", strict_slashes=False)
|
||||||
@@ -161,9 +153,8 @@ def old():
|
|||||||
date_fmt = datetime.datetime.strptime(date, "%y_%m_%d")
|
date_fmt = datetime.datetime.strptime(date, "%y_%m_%d")
|
||||||
date_fmt = date_fmt.strftime("%A, %B %d, %Y")
|
date_fmt = date_fmt.strftime("%A, %B %d, %Y")
|
||||||
response += f"{date_fmt} - /now/{link}\n"
|
response += f"{date_fmt} - /now/{link}\n"
|
||||||
return render_template(
|
return render_template("now.ascii", date="Old Now Pages", content=response, header=get_header())
|
||||||
"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>'
|
||||||
@@ -176,9 +167,7 @@ def old():
|
|||||||
|
|
||||||
html += "</ul>"
|
html += "</ul>"
|
||||||
return render_template(
|
return render_template(
|
||||||
"now/old.html",
|
"now/old.html", handshake_scripts=getHandshakeScript(request.host), now_pages=html
|
||||||
handshake_scripts=getHandshakeScript(request.host),
|
|
||||||
now_pages=html,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -196,7 +185,7 @@ def rss():
|
|||||||
link = page.strip(".html")
|
link = page.strip(".html")
|
||||||
date = datetime.datetime.strptime(link, "%y_%m_%d")
|
date = datetime.datetime.strptime(link, "%y_%m_%d")
|
||||||
date = date.strftime("%A, %B %d, %Y")
|
date = date.strftime("%A, %B %d, %Y")
|
||||||
rss += f"<item><title>What's Happening {date}</title><link>{host}/now/{link}</link><description>Latest updates for {date}</description><guid>{host}/now/{link}</guid></item>"
|
rss += f'<item><title>What\'s Happening {date}</title><link>{host}/now/{link}</link><description>Latest updates for {date}</description><guid>{host}/now/{link}</guid></item>'
|
||||||
rss += "</channel></rss>"
|
rss += "</channel></rss>"
|
||||||
return make_response(rss, 200, {"Content-Type": "application/rss+xml"})
|
return make_response(rss, 200, {"Content-Type": "application/rss+xml"})
|
||||||
|
|
||||||
@@ -207,17 +196,6 @@ def json():
|
|||||||
host = "https://" + request.host
|
host = "https://" + request.host
|
||||||
if ":" in request.host:
|
if ":" in request.host:
|
||||||
host = "http://" + request.host
|
host = "http://" + request.host
|
||||||
now_pages = [
|
now_pages = [{"url": host+"/now/"+page.strip(".html"), "date": datetime.datetime.strptime(page.strip(".html"), "%y_%m_%d").strftime(
|
||||||
{
|
"%A, %B %d, %Y"), "title": "What's Happening "+datetime.datetime.strptime(page.strip(".html"), "%y_%m_%d").strftime("%A, %B %d, %Y")} for page in now_pages]
|
||||||
"url": host + "/now/" + page.strip(".html"),
|
|
||||||
"date": datetime.datetime.strptime(
|
|
||||||
page.strip(".html"), "%y_%m_%d"
|
|
||||||
).strftime("%A, %B %d, %Y"),
|
|
||||||
"title": "What's Happening "
|
|
||||||
+ datetime.datetime.strptime(page.strip(".html"), "%y_%m_%d").strftime(
|
|
||||||
"%A, %B %d, %Y"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
for page in now_pages
|
|
||||||
]
|
|
||||||
return jsonify(now_pages)
|
return jsonify(now_pages)
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ from flask import Blueprint, make_response, request
|
|||||||
from tools import error_response
|
from tools import error_response
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
app = Blueprint("podcast", __name__)
|
app = Blueprint('podcast', __name__)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/ID1")
|
@app.route("/ID1")
|
||||||
def index():
|
def index():
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import binascii
|
|||||||
import base64
|
import base64
|
||||||
import os
|
import os
|
||||||
|
|
||||||
app = Blueprint("sol", __name__)
|
app = Blueprint('sol', __name__)
|
||||||
|
|
||||||
SOLANA_HEADERS = {
|
SOLANA_HEADERS = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Action-Version": "2.4.2",
|
"X-Action-Version": "2.4.2",
|
||||||
"X-Blockchain-Ids": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
"X-Blockchain-Ids": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
|
||||||
}
|
}
|
||||||
|
|
||||||
SOLANA_ADDRESS = None
|
SOLANA_ADDRESS = None
|
||||||
@@ -23,19 +23,15 @@ if os.path.isfile(".well-known/wallets/SOL"):
|
|||||||
address = file.read()
|
address = file.read()
|
||||||
SOLANA_ADDRESS = Pubkey.from_string(address.strip())
|
SOLANA_ADDRESS = Pubkey.from_string(address.strip())
|
||||||
|
|
||||||
|
|
||||||
def create_transaction(sender_address: str, amount: float) -> str:
|
def create_transaction(sender_address: str, amount: float) -> str:
|
||||||
if SOLANA_ADDRESS is None:
|
if SOLANA_ADDRESS is None:
|
||||||
raise ValueError(
|
raise ValueError("SOLANA_ADDRESS is not set. Please ensure the .well-known/wallets/SOL file exists and contains a valid address.")
|
||||||
"SOLANA_ADDRESS is not set. Please ensure the .well-known/wallets/SOL file exists and contains a valid address."
|
|
||||||
)
|
|
||||||
# Create transaction
|
# Create transaction
|
||||||
sender = Pubkey.from_string(sender_address)
|
sender = Pubkey.from_string(sender_address)
|
||||||
transfer_ix = transfer(
|
transfer_ix = transfer(
|
||||||
TransferParams(
|
TransferParams(
|
||||||
from_pubkey=sender,
|
from_pubkey=sender, to_pubkey=SOLANA_ADDRESS, lamports=int(
|
||||||
to_pubkey=SOLANA_ADDRESS,
|
amount * 1000000000)
|
||||||
lamports=int(amount * 1000000000),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
solana_client = Client("https://api.mainnet-beta.solana.com")
|
solana_client = Client("https://api.mainnet-beta.solana.com")
|
||||||
@@ -54,15 +50,11 @@ def create_transaction(sender_address: str, amount: float) -> str:
|
|||||||
base64_string = base64.b64encode(raw_bytes).decode("utf-8")
|
base64_string = base64.b64encode(raw_bytes).decode("utf-8")
|
||||||
return base64_string
|
return base64_string
|
||||||
|
|
||||||
|
|
||||||
def get_solana_address() -> str:
|
def get_solana_address() -> str:
|
||||||
if SOLANA_ADDRESS is None:
|
if SOLANA_ADDRESS is None:
|
||||||
raise ValueError(
|
raise ValueError("SOLANA_ADDRESS is not set. Please ensure the .well-known/wallets/SOL file exists and contains a valid address.")
|
||||||
"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)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/donate", methods=["GET", "OPTIONS"])
|
@app.route("/donate", methods=["GET", "OPTIONS"])
|
||||||
def sol_donate():
|
def sol_donate():
|
||||||
data = {
|
data = {
|
||||||
@@ -111,6 +103,7 @@ def sol_donate_amount(amount):
|
|||||||
|
|
||||||
@app.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:
|
||||||
return jsonify({"message": "Error: No JSON data provided"}), 400, SOLANA_HEADERS
|
return jsonify({"message": "Error: No JSON data provided"}), 400, SOLANA_HEADERS
|
||||||
|
|
||||||
@@ -129,8 +122,4 @@ def sol_donate_post(amount):
|
|||||||
return jsonify({"message": "Error: Amount too small"}), 400, SOLANA_HEADERS
|
return jsonify({"message": "Error: Amount too small"}), 400, SOLANA_HEADERS
|
||||||
|
|
||||||
transaction = create_transaction(sender, amount)
|
transaction = create_transaction(sender, amount)
|
||||||
return (
|
return jsonify({"message": "Success", "transaction": transaction}), 200, SOLANA_HEADERS
|
||||||
jsonify({"message": "Success", "transaction": transaction}),
|
|
||||||
200,
|
|
||||||
SOLANA_HEADERS,
|
|
||||||
)
|
|
||||||
@@ -5,7 +5,7 @@ import requests
|
|||||||
import time
|
import time
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
app = Blueprint("spotify", __name__, url_prefix="/spotify")
|
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")
|
||||||
@@ -21,7 +21,6 @@ ACCESS_TOKEN = None
|
|||||||
REFRESH_TOKEN = os.getenv("SPOTIFY_REFRESH_TOKEN")
|
REFRESH_TOKEN = os.getenv("SPOTIFY_REFRESH_TOKEN")
|
||||||
TOKEN_EXPIRES = 0
|
TOKEN_EXPIRES = 0
|
||||||
|
|
||||||
|
|
||||||
def refresh_access_token():
|
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
|
||||||
@@ -53,7 +52,6 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@app.route("/login")
|
@app.route("/login")
|
||||||
def login():
|
def login():
|
||||||
auth_query = (
|
auth_query = (
|
||||||
@@ -62,7 +60,6 @@ def login():
|
|||||||
)
|
)
|
||||||
return redirect(auth_query)
|
return redirect(auth_query)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/callback")
|
@app.route("/callback")
|
||||||
def callback():
|
def callback():
|
||||||
code = request.args.get("code")
|
code = request.args.get("code")
|
||||||
@@ -79,14 +76,12 @@ def callback():
|
|||||||
response = requests.post(SPOTIFY_TOKEN_URL, data=data)
|
response = requests.post(SPOTIFY_TOKEN_URL, data=data)
|
||||||
token_info = response.json()
|
token_info = response.json()
|
||||||
if "access_token" not in token_info:
|
if "access_token" not in token_info:
|
||||||
return json_response(
|
return json_response(request, {"error": "Failed to obtain token", "details": token_info}, 400)
|
||||||
request, {"error": "Failed to obtain token", "details": token_info}, 400
|
|
||||||
)
|
|
||||||
|
|
||||||
access_token = token_info["access_token"]
|
access_token = token_info["access_token"]
|
||||||
me = requests.get(
|
me = requests.get(
|
||||||
"https://api.spotify.com/v1/me",
|
"https://api.spotify.com/v1/me",
|
||||||
headers={"Authorization": f"Bearer {access_token}"},
|
headers={"Authorization": f"Bearer {access_token}"}
|
||||||
).json()
|
).json()
|
||||||
|
|
||||||
if me.get("id") != ALLOWED_SPOTIFY_USER_ID:
|
if me.get("id") != ALLOWED_SPOTIFY_USER_ID:
|
||||||
@@ -98,14 +93,12 @@ 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"))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/", strict_slashes=False)
|
@app.route("/", strict_slashes=False)
|
||||||
@app.route("/playing")
|
@app.route("/playing")
|
||||||
def currently_playing():
|
def currently_playing():
|
||||||
"""Public endpoint showing your current track."""
|
"""Public endpoint showing your current track."""
|
||||||
track = get_spotify_track()
|
track = get_spotify_track()
|
||||||
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."""
|
||||||
@@ -131,7 +124,7 @@ def get_spotify_track():
|
|||||||
"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),
|
"progress_ms": data.get("progress_ms",0),
|
||||||
"duration_ms": data["item"].get("duration_ms", 1),
|
"duration_ms": data["item"].get("duration_ms",1)
|
||||||
}
|
}
|
||||||
return track
|
return track
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from flask import Blueprint, request
|
from flask import Blueprint, request
|
||||||
from tools import json_response
|
from tools import json_response
|
||||||
|
|
||||||
app = Blueprint("template", __name__)
|
app = Blueprint('template', __name__)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/", strict_slashes=False)
|
@app.route("/", strict_slashes=False)
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
from flask import (
|
from flask import Blueprint, make_response, request, jsonify, send_from_directory, redirect
|
||||||
Blueprint,
|
|
||||||
make_response,
|
|
||||||
request,
|
|
||||||
jsonify,
|
|
||||||
send_from_directory,
|
|
||||||
redirect,
|
|
||||||
)
|
|
||||||
from tools import error_response
|
from tools import error_response
|
||||||
import os
|
import os
|
||||||
|
|
||||||
app = Blueprint("well-known", __name__, url_prefix="/.well-known")
|
app = Blueprint('well-known', __name__, url_prefix='/.well-known')
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
@@ -19,7 +12,7 @@ def index(path):
|
|||||||
|
|
||||||
@app.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(
|
||||||
".well-known/wallets", path, mimetype="application/json"
|
".well-known/wallets", path, mimetype="application/json"
|
||||||
)
|
)
|
||||||
|
|||||||
264
cache_helper.py
264
cache_helper.py
@@ -1,264 +0,0 @@
|
|||||||
"""
|
|
||||||
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 {}
|
|
||||||
23
cleanSite.py
23
cleanSite.py
@@ -1,37 +1,36 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
def cleanSite(path:str):
|
||||||
def cleanSite(path: str):
|
|
||||||
# Check if the file is sitemap.xml
|
# Check if the file is sitemap.xml
|
||||||
if path.endswith("sitemap.xml"):
|
if path.endswith('sitemap.xml'):
|
||||||
# Open the file
|
# Open the file
|
||||||
with open(path, "r") as f:
|
with open(path, 'r') as f:
|
||||||
# Read the content
|
# Read the content
|
||||||
content = f.read()
|
content = f.read()
|
||||||
# Replace all .html with empty string
|
# Replace all .html with empty string
|
||||||
content = content.replace(".html", "")
|
content = content.replace('.html', '')
|
||||||
# Write the content back to the file
|
# Write the content back to the file
|
||||||
with open(path, "w") as f:
|
with open(path, 'w') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
# Skip the file
|
# Skip the file
|
||||||
return
|
return
|
||||||
|
|
||||||
# If the file is not an html file, skip it
|
# If the file is not an html file, skip it
|
||||||
if not path.endswith(".html"):
|
if not path.endswith('.html'):
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
for file in os.listdir(path):
|
for file in os.listdir(path):
|
||||||
cleanSite(path + "/" + file)
|
cleanSite(path + '/' + file)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Open the file
|
# Open the file
|
||||||
with open(path, "r") as f:
|
with open(path, 'r') as f:
|
||||||
# Read and remove all .html
|
# Read and remove all .html
|
||||||
content = f.read().replace('.html"', '"')
|
content = f.read().replace('.html"', '"')
|
||||||
# Write the cleaned content back to the file
|
# Write the cleaned content back to the file
|
||||||
with open(path, "w") as f:
|
with open(path, 'w') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
for file in os.listdir("templates"):
|
for file in os.listdir('templates'):
|
||||||
cleanSite("templates/" + file)
|
cleanSite('templates/' + file)
|
||||||
132
curl.py
132
curl.py
@@ -2,14 +2,13 @@ from flask import render_template
|
|||||||
from tools import 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
|
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
|
||||||
if path.endswith(".html"):
|
if path.endswith(".html"):
|
||||||
@@ -20,34 +19,65 @@ def clean_path(path: str):
|
|||||||
path = "index"
|
path = "index"
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def get_header():
|
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 = get_git_latest_activity()
|
git = requests.get(
|
||||||
repo_name = git["repo"]["name"].lower()
|
"https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",
|
||||||
|
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=16)
|
@lru_cache(maxsize=1)
|
||||||
def get_projects():
|
def get_projects():
|
||||||
projects_data = get_projects_cached(limit=5)
|
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
|
||||||
|
|
||||||
|
# Sort by last updated
|
||||||
|
projectsList = sorted(
|
||||||
|
projects, key=lambda x: x["updated_at"], reverse=True)
|
||||||
projects = ""
|
projects = ""
|
||||||
for project in projects_data:
|
projectNum = 0
|
||||||
projects += f"""[1m{project["name"]}[0m - {project["description"] if project["description"] else "No description"}
|
includedNames = []
|
||||||
{project["html_url"]}
|
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']}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return projects
|
projectNum += 1
|
||||||
|
|
||||||
|
return projects
|
||||||
|
|
||||||
def curl_response(request):
|
def curl_response(request):
|
||||||
# Check if <path>.ascii exists
|
# Check if <path>.ascii exists
|
||||||
@@ -56,81 +86,39 @@ def curl_response(request):
|
|||||||
# Handle special cases
|
# Handle special cases
|
||||||
if path == "index":
|
if path == "index":
|
||||||
# Get current project
|
# Get current project
|
||||||
return (
|
return render_template("index.ascii",repo=get_current_project(), ip=getClientIP(request), spotify=get_spotify_track()), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
render_template(
|
|
||||||
"index.ascii",
|
|
||||||
repo=get_current_project(),
|
|
||||||
ip=getClientIP(request),
|
|
||||||
spotify=get_spotify_track(),
|
|
||||||
),
|
|
||||||
200,
|
|
||||||
{"Content-Type": "text/plain; charset=utf-8"},
|
|
||||||
)
|
|
||||||
if path == "projects":
|
if path == "projects":
|
||||||
# Get projects
|
# Get projects
|
||||||
return (
|
return render_template("projects.ascii",header=get_header(),projects=get_projects()), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
render_template(
|
|
||||||
"projects.ascii", header=get_header(), projects=get_projects()
|
|
||||||
),
|
|
||||||
200,
|
|
||||||
{"Content-Type": "text/plain; charset=utf-8"},
|
|
||||||
)
|
|
||||||
|
|
||||||
if path == "donate":
|
if path == "donate":
|
||||||
# Get donation info
|
# Get donation info
|
||||||
return (
|
return render_template("donate.ascii",header=get_header(),
|
||||||
render_template(
|
HNS=getAddress("HNS"), BTC=getAddress("BTC"),
|
||||||
"donate.ascii",
|
SOL=getAddress("SOL"), ETH=getAddress("ETH")
|
||||||
header=get_header(),
|
), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
HNS=getAddress("HNS"),
|
|
||||||
BTC=getAddress("BTC"),
|
|
||||||
SOL=getAddress("SOL"),
|
|
||||||
ETH=getAddress("ETH"),
|
|
||||||
),
|
|
||||||
200,
|
|
||||||
{"Content-Type": "text/plain; charset=utf-8"},
|
|
||||||
)
|
|
||||||
|
|
||||||
if path == "donate/more":
|
if path == "donate/more":
|
||||||
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()
|
||||||
return (
|
return render_template("donate_more.ascii",header=get_header(),
|
||||||
render_template("donate_more.ascii", header=get_header(), coins=coinList),
|
coins=coinList
|
||||||
200,
|
), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
{"Content-Type": "text/plain; charset=utf-8"},
|
|
||||||
)
|
|
||||||
|
|
||||||
# For other donation pages, fall back to ascii if it exists
|
# For other donation pages, fall back to ascii if it exists
|
||||||
if path.startswith("donate/"):
|
if path.startswith("donate/"):
|
||||||
coin = path.split("/")[1]
|
coin = path.split("/")[1]
|
||||||
address = getAddress(coin)
|
address = getAddress(coin)
|
||||||
if address != "":
|
if address != "":
|
||||||
return (
|
return render_template("donate_coin.ascii",header=get_header(),coin=coin.upper(),address=address), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
render_template(
|
|
||||||
"donate_coin.ascii",
|
|
||||||
header=get_header(),
|
|
||||||
coin=coin.upper(),
|
|
||||||
address=address,
|
|
||||||
),
|
|
||||||
200,
|
|
||||||
{"Content-Type": "text/plain; charset=utf-8"},
|
|
||||||
)
|
|
||||||
|
|
||||||
if path == "tools":
|
if path == "tools":
|
||||||
tools = get_tools_data()
|
tools = get_tools_data()
|
||||||
return (
|
return render_template("tools.ascii",header=get_header(),tools=tools), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
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 (
|
return render_template(f"{path}.ascii",header=get_header()), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
render_template(f"{path}.ascii", header=get_header()),
|
|
||||||
200,
|
|
||||||
{"Content-Type": "text/plain; charset=utf-8"},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Fallback to html if it exists
|
# Fallback to html if it exists
|
||||||
if os.path.exists(f"templates/{path}.html"):
|
if os.path.exists(f"templates/{path}.html"):
|
||||||
@@ -139,10 +127,6 @@ def curl_response(request):
|
|||||||
# Return curl error page
|
# Return curl error page
|
||||||
error = {
|
error = {
|
||||||
"code": 404,
|
"code": 404,
|
||||||
"message": "The requested resource was not found on this server.",
|
"message": "The requested resource was not found on this server."
|
||||||
}
|
}
|
||||||
return (
|
return render_template("error.ascii",header=get_header(),error=error), 404, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
render_template("error.ascii", header=get_header(), error=error),
|
|
||||||
404,
|
|
||||||
{"Content-Type": "text/plain; charset=utf-8"},
|
|
||||||
)
|
|
||||||
57
mail.py
57
mail.py
@@ -21,7 +21,6 @@ import os
|
|||||||
# "body":"G'\''day\nThis is a test email from my website api\n\nRegards,\nNathan.Woodburn/"
|
# "body":"G'\''day\nThis is a test email from my website api\n\nRegards,\nNathan.Woodburn/"
|
||||||
# }'
|
# }'
|
||||||
|
|
||||||
|
|
||||||
def validateSender(email):
|
def validateSender(email):
|
||||||
domains = os.getenv("EMAIL_DOMAINS")
|
domains = os.getenv("EMAIL_DOMAINS")
|
||||||
if not domains:
|
if not domains:
|
||||||
@@ -34,29 +33,37 @@ def validateSender(email):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def sendEmail(data):
|
def sendEmail(data):
|
||||||
fromEmail = "noreply@woodburn.au"
|
fromEmail = "noreply@woodburn.au"
|
||||||
if "from" in data:
|
if "from" in data:
|
||||||
fromEmail = data["from"]
|
fromEmail = data["from"]
|
||||||
|
|
||||||
if not validateSender(fromEmail):
|
if not validateSender(fromEmail):
|
||||||
return jsonify({"status": 400, "message": "Bad request 'from' email invalid"})
|
return jsonify({
|
||||||
|
"status": 400,
|
||||||
|
"message": "Bad request 'from' email invalid"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
if "to" not in data:
|
if "to" not in data:
|
||||||
return jsonify({"status": 400, "message": "Bad request 'to' json data missing"})
|
return jsonify({
|
||||||
|
"status": 400,
|
||||||
|
"message": "Bad request 'to' json data missing"
|
||||||
|
})
|
||||||
to = data["to"]
|
to = data["to"]
|
||||||
|
|
||||||
if "subject" not in data:
|
if "subject" not in data:
|
||||||
return jsonify(
|
return jsonify({
|
||||||
{"status": 400, "message": "Bad request 'subject' json data missing"}
|
"status": 400,
|
||||||
)
|
"message": "Bad request 'subject' json data missing"
|
||||||
|
})
|
||||||
subject = data["subject"]
|
subject = data["subject"]
|
||||||
|
|
||||||
if "body" not in data:
|
if "body" not in data:
|
||||||
return jsonify(
|
return jsonify({
|
||||||
{"status": 400, "message": "Bad request 'body' json data missing"}
|
"status": 400,
|
||||||
)
|
"message": "Bad request 'body' json data missing"
|
||||||
|
})
|
||||||
body = data["body"]
|
body = data["body"]
|
||||||
|
|
||||||
if not re.match(r"[^@]+@[^@]+\.[^@]+", to):
|
if not re.match(r"[^@]+@[^@]+\.[^@]+", to):
|
||||||
@@ -69,15 +76,15 @@ def sendEmail(data):
|
|||||||
raise ValueError("Body cannot be empty.")
|
raise ValueError("Body cannot be empty.")
|
||||||
|
|
||||||
fromName = "Nathan Woodburn"
|
fromName = "Nathan Woodburn"
|
||||||
if "sender" in data:
|
if 'sender' in data:
|
||||||
fromName = data["sender"]
|
fromName = data['sender']
|
||||||
|
|
||||||
# Create the email message
|
# Create the email message
|
||||||
msg = MIMEMultipart()
|
msg = MIMEMultipart()
|
||||||
msg["From"] = formataddr((fromName, fromEmail))
|
msg['From'] = formataddr((fromName, fromEmail))
|
||||||
msg["To"] = to
|
msg['To'] = to
|
||||||
msg["Subject"] = subject
|
msg['Subject'] = subject
|
||||||
msg.attach(MIMEText(body, "plain"))
|
msg.attach(MIMEText(body, 'plain'))
|
||||||
|
|
||||||
# Sending the email
|
# Sending the email
|
||||||
try:
|
try:
|
||||||
@@ -85,12 +92,24 @@ def sendEmail(data):
|
|||||||
user = os.getenv("EMAIL_USER")
|
user = os.getenv("EMAIL_USER")
|
||||||
password = os.getenv("EMAIL_PASS")
|
password = os.getenv("EMAIL_PASS")
|
||||||
if host is None or user is None or password is None:
|
if host is None or user is None or password is None:
|
||||||
return jsonify({"status": 500, "error": "Email server not configured"})
|
return jsonify({
|
||||||
|
"status": 500,
|
||||||
|
"error": "Email server not configured"
|
||||||
|
})
|
||||||
|
|
||||||
with smtplib.SMTP_SSL(host, 465) as server:
|
with smtplib.SMTP_SSL(host, 465) as server:
|
||||||
server.login(user, password)
|
server.login(user, password)
|
||||||
server.sendmail(fromEmail, to, msg.as_string())
|
server.sendmail(fromEmail, to, msg.as_string())
|
||||||
print("Email sent successfully.")
|
print("Email sent successfully.")
|
||||||
return jsonify({"status": 200, "message": "Send email successfully"})
|
return jsonify({
|
||||||
|
"status": 200,
|
||||||
|
"message": "Send email successfully"
|
||||||
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"status": 500, "error": "Sending email failed", "exception": e})
|
return jsonify({
|
||||||
|
"status": 500,
|
||||||
|
"error": "Sending email failed",
|
||||||
|
"exception":e
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
22
main.py
22
main.py
@@ -17,10 +17,9 @@ class GunicornApp(BaseApplication):
|
|||||||
def load(self):
|
def load(self):
|
||||||
return self.application
|
return self.application
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
if __name__ == "__main__":
|
workers = os.getenv('WORKERS')
|
||||||
workers = os.getenv("WORKERS")
|
threads = os.getenv('THREADS')
|
||||||
threads = os.getenv("THREADS")
|
|
||||||
if workers is None:
|
if workers is None:
|
||||||
workers = 1
|
workers = 1
|
||||||
if threads is None:
|
if threads is None:
|
||||||
@@ -28,17 +27,10 @@ if __name__ == "__main__":
|
|||||||
workers = int(workers)
|
workers = int(workers)
|
||||||
threads = int(threads)
|
threads = int(threads)
|
||||||
options = {
|
options = {
|
||||||
"bind": "0.0.0.0:5000",
|
'bind': '0.0.0.0:5000',
|
||||||
"workers": workers,
|
'workers': workers,
|
||||||
"threads": threads,
|
'threads': threads,
|
||||||
}
|
}
|
||||||
gunicorn_app = GunicornApp(app, options)
|
gunicorn_app = GunicornApp(app, options)
|
||||||
print(
|
print('Starting server with ' + str(workers) + ' workers and ' + str(threads) + ' threads', flush=True)
|
||||||
"Starting server with "
|
|
||||||
+ str(workers)
|
|
||||||
+ " workers and "
|
|
||||||
+ str(threads)
|
|
||||||
+ " threads",
|
|
||||||
flush=True,
|
|
||||||
)
|
|
||||||
gunicorn_app.run()
|
gunicorn_app.run()
|
||||||
|
|||||||
@@ -22,10 +22,5 @@ dependencies = [
|
|||||||
"requests>=2.32.5",
|
"requests>=2.32.5",
|
||||||
"solana>=0.36.9",
|
"solana>=0.36.9",
|
||||||
"solders>=0.26.0",
|
"solders>=0.26.0",
|
||||||
]
|
"weasyprint>=66.0",
|
||||||
|
|
||||||
[dependency-groups]
|
|
||||||
dev = [
|
|
||||||
"pre-commit>=4.4.0",
|
|
||||||
"ruff>=0.14.5",
|
|
||||||
]
|
]
|
||||||
|
|||||||
475
requirements.txt
475
requirements.txt
@@ -1,475 +0,0 @@
|
|||||||
# This file was autogenerated by uv via the following command:
|
|
||||||
# uv export --frozen --output-file=requirements.txt
|
|
||||||
annotated-types==0.7.0 \
|
|
||||||
--hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
|
|
||||||
--hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
|
|
||||||
# via pydantic
|
|
||||||
ansi2html==1.9.2 \
|
|
||||||
--hash=sha256:3453bf87535d37b827b05245faaa756dbab4ec3d69925e352b6319c3c955c0a5 \
|
|
||||||
--hash=sha256:dccb75aa95fb018e5d299be2b45f802952377abfdce0504c17a6ee6ef0a420c5
|
|
||||||
# via nathanwoodburn-github-io
|
|
||||||
anyio==4.11.0 \
|
|
||||||
--hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \
|
|
||||||
--hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4
|
|
||||||
# via
|
|
||||||
# cloudflare
|
|
||||||
# httpx
|
|
||||||
beautifulsoup4==4.14.2 \
|
|
||||||
--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
|
|
||||||
249
server.py
249
server.py
@@ -18,30 +18,10 @@ 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 import now, blog, wellknown, api, podcast, acme, spotify
|
from blueprints import now, blog, wellknown, api, podcast, acme, spotify
|
||||||
from tools import (
|
from tools import isCLI, isCrawler, getAddress, getFilePath, error_response, getClientIP, json_response, getHandshakeScript, get_tools_data
|
||||||
isCLI,
|
|
||||||
isCrawler,
|
|
||||||
getAddress,
|
|
||||||
getFilePath,
|
|
||||||
error_response,
|
|
||||||
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)
|
||||||
@@ -70,14 +50,25 @@ REDIRECT_ROUTES = {
|
|||||||
"/meeting": "https://cloud.woodburn.au/apps/calendar/appointment/PamrmmspWJZr",
|
"/meeting": "https://cloud.woodburn.au/apps/calendar/appointment/PamrmmspWJZr",
|
||||||
"/appointment": "https://cloud.woodburn.au/apps/calendar/appointment/PamrmmspWJZr",
|
"/appointment": "https://cloud.woodburn.au/apps/calendar/appointment/PamrmmspWJZr",
|
||||||
}
|
}
|
||||||
DOWNLOAD_ROUTES = {"pgp": "data/nathanwoodburn.asc"}
|
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 = [site for site in SITES if "enabled" not in site or site["enabled"]]
|
SITES = [
|
||||||
|
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
|
||||||
|
|
||||||
@@ -123,13 +114,6 @@ 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():
|
||||||
@@ -169,7 +153,6 @@ 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
|
||||||
|
|
||||||
@@ -194,7 +177,6 @@ def manifest():
|
|||||||
def serviceWorker():
|
def serviceWorker():
|
||||||
return send_from_directory("pwa", "sw.js")
|
return send_from_directory("pwa", "sw.js")
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
@@ -203,14 +185,12 @@ def serviceWorker():
|
|||||||
def links():
|
def links():
|
||||||
return render_template("link.html")
|
return render_template("link.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/actions.json")
|
@app.route("/actions.json")
|
||||||
def sol_actions():
|
def sol_actions():
|
||||||
return jsonify(
|
return jsonify(
|
||||||
{"rules": [{"pathPattern": "/donate**", "apiPath": "/api/v1/donate**"}]}
|
{"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
|
||||||
@@ -220,7 +200,6 @@ def api_legacy(function):
|
|||||||
return redirect(f"/api/v1/{function}", code=301)
|
return redirect(f"/api/v1/{function}", code=301)
|
||||||
return error_response(request, message="404 Not Found", code=404)
|
return error_response(request, message="404 Not Found", code=404)
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region Main routes
|
# region Main routes
|
||||||
@@ -228,6 +207,9 @@ def api_legacy(function):
|
|||||||
|
|
||||||
@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")
|
||||||
@@ -258,22 +240,79 @@ def index():
|
|||||||
resp.set_cookie("loaded", "true", max_age=604800)
|
resp.set_cookie("loaded", "true", max_age=604800)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
# Use cached git data
|
try:
|
||||||
git = get_git_latest_activity()
|
git = requests.get(
|
||||||
repo_name = git["repo"]["name"].lower()
|
"https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",
|
||||||
|
headers={"Authorization": os.getenv("GIT_AUTH")},
|
||||||
|
)
|
||||||
|
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"]
|
||||||
|
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}")
|
||||||
|
|
||||||
# Use cached projects data
|
# Get only repo names for the newest updates
|
||||||
projects = get_projects(limit=3)
|
if PROJECTS == [] or PROJECTS_UPDATED < (datetime.datetime.now() - datetime.timedelta(
|
||||||
|
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/"
|
||||||
@@ -281,9 +320,8 @@ 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 using cached config
|
# Get time
|
||||||
nc_config = get_nc_config()
|
timezone_offset = datetime.timedelta(hours=NC_CONFIG["time-zone"])
|
||||||
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)
|
||||||
|
|
||||||
@@ -306,7 +344,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")
|
||||||
@@ -326,9 +364,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"},
|
||||||
@@ -337,7 +375,6 @@ def index():
|
|||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
# region Donate
|
# region Donate
|
||||||
|
|
||||||
|
|
||||||
@@ -350,25 +387,31 @@ def donate():
|
|||||||
coinList = [file for file in coinList if file[0] != "."]
|
coinList = [file for file in coinList if file[0] != "."]
|
||||||
coinList.sort()
|
coinList.sort()
|
||||||
|
|
||||||
tokenList = get_wallet_tokens()
|
tokenList = []
|
||||||
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:
|
||||||
coin_name = coinNames.get(file, file)
|
if file in coinNames:
|
||||||
display_style = "" if file.lower() in default_coins else "display:none;"
|
coins += f'<a class="dropdown-item" style="{"display:none;" if file.lower() not in default_coins else ""}" href="?c={file.lower()}">{coinNames[file]}</a>'
|
||||||
coins += f'<a class="dropdown-item" style="{display_style}" href="?c={file.lower()}">{coin_name}</a>'
|
else:
|
||||||
|
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:
|
||||||
chain_display = f" on {token['chain']}" if token["chain"] != "null" else ""
|
if token["chain"] != "null":
|
||||||
symbol_display = (
|
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>'
|
||||||
f" ({token['symbol']}{chain_display})"
|
else:
|
||||||
if token["symbol"] != token["name"]
|
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>'
|
||||||
else chain_display
|
|
||||||
)
|
|
||||||
coins += f'<a class="dropdown-item" style="display:none;" href="?t={token["symbol"].lower()}&c={token["chain"].lower()}">{token["name"]}{symbol_display}</a>'
|
|
||||||
|
|
||||||
crypto = request.args.get("c")
|
crypto = request.args.get("c")
|
||||||
if not crypto:
|
if not crypto:
|
||||||
@@ -395,6 +438,7 @@ 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 = ""
|
||||||
@@ -404,16 +448,10 @@ 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 {coin_display}:"
|
cryptoHTML += f"<br>Donate with {coinNames[crypto] if crypto in coinNames else crypto}:"
|
||||||
else:
|
else:
|
||||||
token_symbol = (
|
cryptoHTML += f'<br>Donate with {token["name"]} {"("+token["symbol"]+") " if token["symbol"] != token["name"] else ""}on {crypto}:'
|
||||||
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:
|
||||||
@@ -421,27 +459,25 @@ def donate():
|
|||||||
elif token:
|
elif token:
|
||||||
if "address" in token:
|
if "address" in token:
|
||||||
address = token["address"]
|
address = token["address"]
|
||||||
token_symbol = (
|
cryptoHTML += f'<br>Donate with {token["name"]} {"("+token["symbol"]+")" if token["symbol"] != token["name"] else ""}{" on "+crypto if crypto != "NULL" else ""}:'
|
||||||
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>"
|
||||||
|
|
||||||
domains = get_wallet_domains()
|
if os.path.isfile(".well-known/wallets/.domains"):
|
||||||
|
# Get json of all domains
|
||||||
|
with open(".well-known/wallets/.domains") as file:
|
||||||
|
domains = file.read()
|
||||||
|
domains = json.loads(domains)
|
||||||
|
|
||||||
if crypto in domains:
|
if crypto in domains:
|
||||||
domain = domains[crypto]
|
domain = domains[crypto]
|
||||||
cryptoHTML += "<br>Or send to this domain on compatible wallets:<br>"
|
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>'
|
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/'
|
||||||
@@ -484,30 +520,27 @@ 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(error_correction=ERROR_CORRECT_H, box_size=10, border=2)
|
qr = qrcode.QRCode(
|
||||||
|
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"
|
fill_color="black", back_color="white").convert('RGB') # type: ignore
|
||||||
).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 = (
|
pos = ((qr_image.size[0] - logo.size[0]) // 2,
|
||||||
(qr_image.size[0] - logo.size[0]) // 2,
|
(qr_image.size[1] - logo.size[1]) // 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
|
||||||
|
|
||||||
|
|
||||||
@@ -547,18 +580,15 @@ def hosting_post():
|
|||||||
|
|
||||||
# Check email rate limit
|
# Check email rate limit
|
||||||
if email in EMAIL_REQUEST_COUNT:
|
if email in EMAIL_REQUEST_COUNT:
|
||||||
if (
|
if (current_time - EMAIL_REQUEST_COUNT[email]["last_reset"]) > RATE_LIMIT_WINDOW:
|
||||||
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] = {"count": 1, "last_reset": current_time}
|
EMAIL_REQUEST_COUNT[email] = {
|
||||||
|
"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(
|
return json_response(request, "Rate limit exceeded. Please try again later.", 429)
|
||||||
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}
|
||||||
@@ -572,9 +602,7 @@ 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(
|
return json_response(request, "Rate limit exceeded. Please try again later.", 429)
|
||||||
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}
|
||||||
@@ -633,13 +661,12 @@ def hosting_post():
|
|||||||
return json_response(request, "Failed to send enquiry", 500)
|
return json_response(request, "Failed to send enquiry", 500)
|
||||||
return json_response(request, "Enquiry sent", 200)
|
return json_response(request, "Enquiry sent", 200)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/resume")
|
@app.route("/resume")
|
||||||
def resume():
|
def resume():
|
||||||
# Check if arg for support is passed
|
# Check if arg for support is passed
|
||||||
support = request.args.get("support")
|
support = request.args.get("support")
|
||||||
return render_template("resume.html", support=support)
|
return render_template(
|
||||||
|
"resume.html", support=support)
|
||||||
|
|
||||||
@app.route("/resume.pdf")
|
@app.route("/resume.pdf")
|
||||||
def resume_pdf():
|
def resume_pdf():
|
||||||
@@ -656,14 +683,13 @@ def tools():
|
|||||||
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)
|
||||||
|
|
||||||
@@ -676,23 +702,17 @@ 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(
|
return render_template(path, handshake_scripts=getHandshakeScript(request.host), sites=SITES)
|
||||||
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",
|
path + ".html", handshake_scripts=getHandshakeScript(request.host), sites=SITES
|
||||||
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",
|
path.strip("/") + ".html", handshake_scripts=getHandshakeScript(request.host), sites=SITES
|
||||||
handshake_scripts=getHandshakeScript(request.host),
|
|
||||||
sites=SITES,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Try to find a file matching
|
# Try to find a file matching
|
||||||
@@ -709,7 +729,6 @@ 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/resume.min.css
vendored
2
templates/assets/css/resume.min.css
vendored
@@ -1 +1 @@
|
|||||||
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}
|
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}
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -38,15 +38,7 @@
|
|||||||
<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">
|
<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="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;">
|
<body style="font-family: 'Noto Sans', sans-serif;">
|
||||||
<strong>Mobile detected!</strong>
|
|
||||||
<a href="/resume.pdf" style="color: white; text-decoration: underline;">View PDF version instead</a>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
if (window.innerWidth <= 768) {
|
|
||||||
document.getElementById('mobile-pdf-notice').style.display = 'block';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<div class="container-fluid h-100">
|
<div class="container-fluid h-100">
|
||||||
<div class="row h-100 resume-row">
|
<div class="row h-100 resume-row">
|
||||||
<div class="col-md-4 resume-column resume-column-left">
|
<div class="col-md-4 resume-column resume-column-left">
|
||||||
@@ -119,7 +111,7 @@ if (window.innerWidth <= 768) {
|
|||||||
<hr class="title-hr">
|
<hr class="title-hr">
|
||||||
</div>
|
</div>
|
||||||
<div class="l-summary">
|
<div class="l-summary">
|
||||||
<h1 class="r-heading1">Summary</h1>
|
<h1 class="r-heading1">Profile</h1>
|
||||||
<hr class="hr-l-primary">
|
<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>
|
<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>
|
||||||
@@ -127,14 +119,6 @@ if (window.innerWidth <= 768) {
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<h1 class="r-heading1">Experience</h1>
|
<h1 class="r-heading1">Experience</h1>
|
||||||
<hr class="hr-l-primary">
|
<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">
|
<div class="noprintbreak">
|
||||||
<h4 class="l-heading2 float-right">Oct 2022 - Jun 2025</h4>
|
<h4 class="l-heading2 float-right">Oct 2022 - Jun 2025</h4>
|
||||||
<h4 class="l-heading2">Technical Support Specialist</h4>
|
<h4 class="l-heading2">Technical Support Specialist</h4>
|
||||||
|
|||||||
@@ -72,9 +72,6 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ 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" matches "^(127|172).(0|17).0.1$"
|
jsonpath "$.ip" == "127.0.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
|
||||||
|
|||||||
53
tools.py
53
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
|
from functools import lru_cache as cache
|
||||||
import datetime
|
import datetime
|
||||||
from typing import Optional, Dict, Union, Tuple
|
from typing import Optional, Dict, Union, Tuple
|
||||||
import re
|
import re
|
||||||
@@ -24,10 +24,16 @@ CRAWLERS = [
|
|||||||
"Exabot",
|
"Exabot",
|
||||||
"facebot",
|
"facebot",
|
||||||
"ia_archiver",
|
"ia_archiver",
|
||||||
"Twitterbot",
|
"Twitterbot"
|
||||||
]
|
]
|
||||||
|
|
||||||
CLI_AGENTS = ["curl", "hurl", "xh", "Posting", "HTTPie", "nushell"]
|
CLI_AGENTS = [
|
||||||
|
"curl",
|
||||||
|
"hurl",
|
||||||
|
"xh",
|
||||||
|
"Posting",
|
||||||
|
"HTTPie"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def getClientIP(request: Request) -> str:
|
def getClientIP(request: Request) -> str:
|
||||||
@@ -49,8 +55,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.
|
||||||
@@ -109,8 +114,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.
|
||||||
@@ -130,8 +134,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.
|
||||||
@@ -146,8 +149,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.
|
||||||
@@ -166,7 +168,7 @@ def getAddress(coin: str) -> str:
|
|||||||
return address
|
return address
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=256)
|
@cache
|
||||||
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.
|
||||||
@@ -184,9 +186,7 @@ def getFilePath(name: str, path: str) -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def json_response(
|
def json_response(request: Request, message: Union[str, Dict] = "404 Not Found", code: int = 404):
|
||||||
request: Request, message: Union[str, Dict] = "404 Not Found", code: int = 404
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Create a JSON response with standard formatting.
|
Create a JSON response with standard formatting.
|
||||||
|
|
||||||
@@ -204,20 +204,17 @@ def json_response(
|
|||||||
message["ip"] = getClientIP(request)
|
message["ip"] = getClientIP(request)
|
||||||
return jsonify(message), code
|
return jsonify(message), code
|
||||||
|
|
||||||
return jsonify(
|
return jsonify({
|
||||||
{
|
|
||||||
"status": code,
|
"status": code,
|
||||||
"message": message,
|
"message": message,
|
||||||
"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",
|
||||||
code: int = 404,
|
code: int = 404,
|
||||||
force_json: bool = False,
|
force_json: bool = False
|
||||||
) -> Union[Tuple[Dict, int], object]:
|
) -> Union[Tuple[Dict, int], object]:
|
||||||
"""
|
"""
|
||||||
Create an error response in JSON or HTML format.
|
Create an error response in JSON or HTML format.
|
||||||
@@ -235,12 +232,10 @@ def error_response(
|
|||||||
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
|
||||||
template_name = (
|
template_name = f"{code}.html" if os.path.isfile(
|
||||||
f"{code}.html" if os.path.isfile(f"templates/{code}.html") else "404.html"
|
f"templates/{code}.html") else "404.html"
|
||||||
)
|
response = make_response(render_template(
|
||||||
response = make_response(
|
template_name, code=code, message=message), code)
|
||||||
render_template(template_name, code=code, message=message), code
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add message to response headers
|
# Add message to response headers
|
||||||
response.headers["X-Error-Message"] = message
|
response.headers["X-Error-Message"] = message
|
||||||
@@ -264,7 +259,8 @@ def parse_date(date_groups: list[str]) -> str | None:
|
|||||||
date_str = " ".join(date_groups).strip()
|
date_str = " ".join(date_groups).strip()
|
||||||
|
|
||||||
# Remove ordinal suffixes
|
# Remove ordinal suffixes
|
||||||
date_str = re.sub(r"(\d+)(st|nd|rd|th)", r"\1", date_str, flags=re.IGNORECASE)
|
date_str = re.sub(r'(\d+)(st|nd|rd|th)', r'\1',
|
||||||
|
date_str, flags=re.IGNORECASE)
|
||||||
|
|
||||||
# Parse with dateutil, default day=1 if missing
|
# Parse with dateutil, default day=1 if missing
|
||||||
dt = parse(date_str, default=datetime.datetime(1900, 1, 1))
|
dt = parse(date_str, default=datetime.datetime(1900, 1, 1))
|
||||||
@@ -278,7 +274,6 @@ def parse_date(date_groups: list[str]) -> str | None:
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_tools_data():
|
def get_tools_data():
|
||||||
with open("data/tools.json", "r") as f:
|
with open("data/tools.json", "r") as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
348
uv.lock
generated
348
uv.lock
generated
@@ -55,6 +55,43 @@ 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" },
|
{ 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 = "brotli"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270, upload-time = "2023-09-07T14:05:41.643Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681, upload-time = "2024-10-18T12:32:34.942Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475, upload-time = "2024-10-18T12:32:36.485Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173, upload-time = "2024-10-18T12:32:37.978Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/1d/e6ca79c96ff5b641df6097d299347507d39a9604bde8915e76bf026d6c77/Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", size = 2943803, upload-time = "2024-10-18T12:32:39.606Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/a3/d98d2472e0130b7dd3acdbb7f390d478123dbf62b7d32bda5c830a96116d/Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", size = 2918946, upload-time = "2024-10-18T12:32:41.679Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/a5/c69e6d272aee3e1423ed005d8915a7eaa0384c7de503da987f2d224d0721/Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", size = 2845707, upload-time = "2024-10-18T12:32:43.478Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/9f/4149d38b52725afa39067350696c09526de0125ebfbaab5acc5af28b42ea/Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", size = 2936231, upload-time = "2024-10-18T12:32:45.224Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/5a/145de884285611838a16bebfdb060c231c52b8f84dfbe52b852a15780386/Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", size = 2848157, upload-time = "2024-10-18T12:32:46.894Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/ae/408b6bfb8525dadebd3b3dd5b19d631da4f7d46420321db44cd99dcf2f2c/Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", size = 3035122, upload-time = "2024-10-18T12:32:48.844Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206, upload-time = "2024-10-18T12:32:51.198Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/f0/a61d9262cd01351df22e57ad7c34f66794709acab13f34be2675f45bf89d/Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", size = 333804, upload-time = "2024-10-18T12:32:52.661Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/c1/ec214e9c94000d1c1974ec67ced1c970c148aa6b8d8373066123fc3dbf06/Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", size = 358517, upload-time = "2024-10-18T12:32:54.066Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "brotlicffi"
|
||||||
|
version = "1.1.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cffi" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/95/9d/70caa61192f570fcf0352766331b735afa931b4c6bc9a348a0925cc13288/brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13", size = 465192, upload-time = "2023-09-14T14:22:40.707Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/11/7b96009d3dcc2c931e828ce1e157f03824a69fb728d06bfd7b2fc6f93718/brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851", size = 453786, upload-time = "2023-09-14T14:21:57.72Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/e6/a8f46f4a4ee7856fbd6ac0c6fb0dc65ed181ba46cd77875b8d9bbe494d9e/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b", size = 2911165, upload-time = "2023-09-14T14:21:59.613Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/20/201559dff14e83ba345a5ec03335607e47467b6633c210607e693aefac40/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814", size = 2927895, upload-time = "2023-09-14T14:22:01.22Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/15/695b1409264143be3c933f708a3f81d53c4a1e1ebbc06f46331decbf6563/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820", size = 2851834, upload-time = "2023-09-14T14:22:03.571Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/40/b961a702463b6005baf952794c2e9e0099bde657d0d7e007f923883b907f/brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb", size = 341731, upload-time = "2023-09-14T14:22:05.74Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/fa/5408a03c041114ceab628ce21766a4ea882aa6f6f0a800e04ee3a30ec6b9/brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613", size = 366783, upload-time = "2023-09-14T14:22:07.096Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cachetools"
|
name = "cachetools"
|
||||||
version = "6.2.1"
|
version = "6.2.1"
|
||||||
@@ -74,12 +111,48 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfgv"
|
name = "cffi"
|
||||||
version = "3.4.0"
|
version = "2.0.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
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" }
|
dependencies = [
|
||||||
|
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||||
wheels = [
|
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" },
|
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -181,12 +254,16 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "distlib"
|
name = "cssselect2"
|
||||||
version = "0.4.0"
|
version = "0.8.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
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" }
|
dependencies = [
|
||||||
|
{ name = "tinycss2" },
|
||||||
|
{ name = "webencodings" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9f/86/fd7f58fc498b3166f3a7e8e0cddb6e620fe1da35b02248b1bd59e95dbaaa/cssselect2-0.8.0.tar.gz", hash = "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a", size = 35716, upload-time = "2025-03-05T14:46:07.988Z" }
|
||||||
wheels = [
|
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" },
|
{ url = "https://files.pythonhosted.org/packages/0f/e7/aa315e6a749d9b96c2504a1ba0ba031ba2d0517e972ce22682e3fccecb09/cssselect2-0.8.0-py3-none-any.whl", hash = "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e", size = 15454, upload-time = "2025-03-05T14:46:06.463Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -198,15 +275,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "flask"
|
name = "flask"
|
||||||
version = "3.1.2"
|
version = "3.1.2"
|
||||||
@@ -237,6 +305,46 @@ 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" },
|
{ 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 = "fonttools"
|
||||||
|
version = "4.60.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb", size = 2825777, upload-time = "2025-09-29T21:12:01.22Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/8a/de9cc0540f542963ba5e8f3a1f6ad48fa211badc3177783b9d5cadf79b5d/fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4", size = 2348080, upload-time = "2025-09-29T21:12:03.785Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/8b/371ab3cec97ee3fe1126b3406b7abd60c8fec8975fd79a3c75cdea0c3d83/fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c", size = 4903082, upload-time = "2025-09-29T21:12:06.382Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77", size = 4960125, upload-time = "2025-09-29T21:12:09.314Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/37/f3b840fcb2666f6cb97038793606bdd83488dca2d0b0fc542ccc20afa668/fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199", size = 4901454, upload-time = "2025-09-29T21:12:11.931Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/9e/eb76f77e82f8d4a46420aadff12cec6237751b0fb9ef1de373186dcffb5f/fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c", size = 5044495, upload-time = "2025-09-29T21:12:15.241Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/b3/cede8f8235d42ff7ae891bae8d619d02c8ac9fd0cfc450c5927a6200c70d/fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272", size = 2217028, upload-time = "2025-09-29T21:12:17.96Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/4d/b022c1577807ce8b31ffe055306ec13a866f2337ecee96e75b24b9b753ea/fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac", size = 2266200, upload-time = "2025-09-29T21:12:20.14Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/83/752ca11c1aa9a899b793a130f2e466b79ea0cf7279c8d79c178fc954a07b/fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3", size = 2822830, upload-time = "2025-09-29T21:12:24.406Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/17/bbeab391100331950a96ce55cfbbff27d781c1b85ebafb4167eae50d9fe3/fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85", size = 2345524, upload-time = "2025-09-29T21:12:26.819Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/2e/d4831caa96d85a84dd0da1d9f90d81cec081f551e0ea216df684092c6c97/fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537", size = 4843490, upload-time = "2025-09-29T21:12:29.123Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/13/5e2ea7c7a101b6fc3941be65307ef8df92cbbfa6ec4804032baf1893b434/fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003", size = 4944184, upload-time = "2025-09-29T21:12:31.414Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/2b/cf9603551c525b73fc47c52ee0b82a891579a93d9651ed694e4e2cd08bb8/fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08", size = 4890218, upload-time = "2025-09-29T21:12:33.936Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/2f/933d2352422e25f2376aae74f79eaa882a50fb3bfef3c0d4f50501267101/fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99", size = 4999324, upload-time = "2025-09-29T21:12:36.637Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/99/234594c0391221f66216bc2c886923513b3399a148defaccf81dc3be6560/fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6", size = 2220861, upload-time = "2025-09-29T21:12:39.108Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/1d/edb5b23726dde50fc4068e1493e4fc7658eeefcaf75d4c5ffce067d07ae5/fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987", size = 2270934, upload-time = "2025-09-29T21:12:41.339Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/da/1392aaa2170adc7071fe7f9cfd181a5684a7afcde605aebddf1fb4d76df5/fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299", size = 2894340, upload-time = "2025-09-29T21:12:43.774Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/a7/3b9f16e010d536ce567058b931a20b590d8f3177b2eda09edd92e392375d/fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01", size = 2375073, upload-time = "2025-09-29T21:12:46.437Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/b5/e9bcf51980f98e59bb5bb7c382a63c6f6cac0eec5f67de6d8f2322382065/fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801", size = 4849758, upload-time = "2025-09-29T21:12:48.694Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/dc/1d2cf7d1cba82264b2f8385db3f5960e3d8ce756b4dc65b700d2c496f7e9/fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc", size = 5085598, upload-time = "2025-09-29T21:12:51.081Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/4d/279e28ba87fb20e0c69baf72b60bbf1c4d873af1476806a7b5f2b7fac1ff/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc", size = 4957603, upload-time = "2025-09-29T21:12:53.423Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/d4/ff19976305e0c05aa3340c805475abb00224c954d3c65e82c0a69633d55d/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed", size = 4974184, upload-time = "2025-09-29T21:12:55.962Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/22/8553ff6166f5cd21cfaa115aaacaa0dc73b91c079a8cfd54a482cbc0f4f5/fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259", size = 2282241, upload-time = "2025-09-29T21:12:58.179Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/cb/fa7b4d148e11d5a72761a22e595344133e83a9507a4c231df972e657579b/fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c", size = 2345760, upload-time = "2025-09-29T21:13:00.375Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
woff = [
|
||||||
|
{ name = "brotli", marker = "platform_python_implementation == 'CPython'" },
|
||||||
|
{ name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" },
|
||||||
|
{ name = "zopfli" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gunicorn"
|
name = "gunicorn"
|
||||||
version = "23.0.0"
|
version = "23.0.0"
|
||||||
@@ -286,15 +394,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.11"
|
version = "3.11"
|
||||||
@@ -417,12 +516,7 @@ dependencies = [
|
|||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
{ name = "solana" },
|
{ name = "solana" },
|
||||||
{ name = "solders" },
|
{ name = "solders" },
|
||||||
]
|
{ name = "weasyprint" },
|
||||||
|
|
||||||
[package.dev-dependencies]
|
|
||||||
dev = [
|
|
||||||
{ name = "pre-commit" },
|
|
||||||
{ name = "ruff" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
@@ -444,21 +538,7 @@ requires-dist = [
|
|||||||
{ name = "requests", specifier = ">=2.32.5" },
|
{ name = "requests", specifier = ">=2.32.5" },
|
||||||
{ name = "solana", specifier = ">=0.36.9" },
|
{ name = "solana", specifier = ">=0.36.9" },
|
||||||
{ name = "solders", specifier = ">=0.26.0" },
|
{ name = "solders", specifier = ">=0.26.0" },
|
||||||
]
|
{ name = "weasyprint", specifier = ">=66.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]]
|
[[package]]
|
||||||
@@ -529,28 +609,12 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "pycparser"
|
||||||
version = "4.5.0"
|
version = "2.23"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
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" }
|
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
|
||||||
wheels = [
|
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" },
|
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
|
||||||
]
|
|
||||||
|
|
||||||
[[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]]
|
[[package]]
|
||||||
@@ -617,6 +681,15 @@ wheels = [
|
|||||||
{ 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" },
|
{ 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 = "pydyf"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2e/c2/97fc6ce4ce0045080dc99446def812081b57750ed8aa67bfdfafa4561fe5/pydyf-0.11.0.tar.gz", hash = "sha256:394dddf619cca9d0c55715e3c55ea121a9bf9cbc780cdc1201a2427917b86b64", size = 17769, upload-time = "2024-07-12T12:26:51.95Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/ac/d5db977deaf28c6ecbc61bbca269eb3e8f0b3a1f55c8549e5333e606e005/pydyf-0.11.0-py3-none-any.whl", hash = "sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3", size = 8104, upload-time = "2024-07-12T12:26:49.896Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.19.2"
|
version = "2.19.2"
|
||||||
@@ -626,6 +699,15 @@ 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" },
|
{ 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 = "pyphen"
|
||||||
|
version = "0.17.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/69/56/e4d7e1bd70d997713649c5ce530b2d15a5fc2245a74ca820fc2d51d89d4d/pyphen-0.17.2.tar.gz", hash = "sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3", size = 2079470, upload-time = "2025-01-20T13:18:36.296Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/1f/c2142d2edf833a90728e5cdeb10bdbdc094dde8dbac078cee0cf33f5e11b/pyphen-0.17.2-py3-none-any.whl", hash = "sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd", size = 2079358, upload-time = "2025-01-20T13:18:29.629Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
version = "2.9.0.post0"
|
version = "2.9.0.post0"
|
||||||
@@ -647,42 +729,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "qrcode"
|
name = "qrcode"
|
||||||
version = "8.2"
|
version = "8.2"
|
||||||
@@ -710,32 +756,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "six"
|
name = "six"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
@@ -799,6 +819,30 @@ 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" },
|
{ 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 = "tinycss2"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "webencodings" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyhtml5"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "webencodings" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fd/03/6111ed99e9bf7dfa1c30baeef0e0fb7e0bd387bd07f8e5b270776fe1de3f/tinyhtml5-2.0.0.tar.gz", hash = "sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc", size = 179507, upload-time = "2024-10-29T15:37:14.078Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/de/27c57899297163a4a84104d5cec0af3b1ac5faf62f44667e506373c6b8ce/tinyhtml5-2.0.0-py3-none-any.whl", hash = "sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e", size = 39793, upload-time = "2024-10-29T15:37:11.743Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.15.0"
|
version = "4.15.0"
|
||||||
@@ -830,17 +874,31 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "virtualenv"
|
name = "weasyprint"
|
||||||
version = "20.35.4"
|
version = "66.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "distlib" },
|
{ name = "cffi" },
|
||||||
{ name = "filelock" },
|
{ name = "cssselect2" },
|
||||||
{ name = "platformdirs" },
|
{ name = "fonttools", extra = ["woff"] },
|
||||||
|
{ name = "pillow" },
|
||||||
|
{ name = "pydyf" },
|
||||||
|
{ name = "pyphen" },
|
||||||
|
{ name = "tinycss2" },
|
||||||
|
{ name = "tinyhtml5" },
|
||||||
]
|
]
|
||||||
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" }
|
sdist = { url = "https://files.pythonhosted.org/packages/32/99/480b5430b7eb0916e7d5df1bee7d9508b28b48fee28da894d0a050e0e930/weasyprint-66.0.tar.gz", hash = "sha256:da71dc87dc129ac9cffdc65e5477e90365ab9dbae45c744014ec1d06303dde40", size = 504224, upload-time = "2025-07-24T11:44:42.771Z" }
|
||||||
wheels = [
|
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" },
|
{ url = "https://files.pythonhosted.org/packages/0f/d1/c5d9b341bf3d556c1e4c6566b3efdda0b1bb175510aa7b09dd3eee246923/weasyprint-66.0-py3-none-any.whl", hash = "sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0", size = 301965, upload-time = "2025-07-24T11:44:40.968Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webencodings"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -874,3 +932,21 @@ sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2
|
|||||||
wheels = [
|
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" },
|
{ 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" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zopfli"
|
||||||
|
version = "0.2.3.post1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5e/7c/a8f6696e694709e2abcbccd27d05ef761e9b6efae217e11d977471555b62/zopfli-0.2.3.post1.tar.gz", hash = "sha256:96484dc0f48be1c5d7ae9f38ed1ce41e3675fd506b27c11a6607f14b49101e99", size = 175629, upload-time = "2024-10-18T15:42:05.946Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/24/0e552e2efce9a20625b56e9609d1e33c2966be33fc008681121ec267daec/zopfli-0.2.3.post1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecb7572df5372abce8073df078207d9d1749f20b8b136089916a4a0868d56051", size = 295485, upload-time = "2024-10-18T15:41:12.57Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/83/b2564369fb98797a617fe2796097b1d719a4937234375757ad2a3febc04b/zopfli-0.2.3.post1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1cf720896d2ce998bc8e051d4b4ce0d8bec007aab6243102e8e1d22a0b2fb3f", size = 163000, upload-time = "2024-10-18T15:41:13.743Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/55/81d419739c2aab35e19b58bce5498dcb58e6446e5eb69f2d3c748b1c9151/zopfli-0.2.3.post1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aad740b4d4fcbaaae4887823925166ffd062db3b248b3f432198fc287381d1a", size = 823699, upload-time = "2024-10-18T15:41:14.874Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/91/89f07c8ea3c9bc64099b3461627b07a8384302235ee0f357eaa86f98f509/zopfli-0.2.3.post1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6617fb10f9e4393b331941861d73afb119cd847e88e4974bdbe8068ceef3f73f", size = 826612, upload-time = "2024-10-18T15:41:16.069Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/31/46670fc0c7805d42bc89702440fa9b73491d68abbc39e28d687180755178/zopfli-0.2.3.post1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a53b18797cdef27e019db595d66c4b077325afe2fd62145953275f53d84ce40c", size = 851148, upload-time = "2024-10-18T15:41:17.403Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/00/71ad39277bbb88f9fd20fb786bd3ff2ea4025c53b31652a0da796fb546cd/zopfli-0.2.3.post1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b78008a69300d929ca2efeffec951b64a312e9a811e265ea4a907ab546d79fa6", size = 1754215, upload-time = "2024-10-18T15:41:18.661Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/4e/e542c508d20c3dfbef1b90fcf726f824f505e725747f777b0b7b7d1deb95/zopfli-0.2.3.post1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa5f90d6298bda02a95bc8dc8c3c19004d5a4e44bda00b67ca7431d857b4b54", size = 1905988, upload-time = "2024-10-18T15:41:19.933Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/a5/817ac1ecc888723e91dc172e8c6eeab9f48a1e52285803b965084e11bbd5/zopfli-0.2.3.post1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2768c877f76c8a0e7519b1c86c93757f3c01492ddde55751e9988afb7eff64e1", size = 1835907, upload-time = "2024-10-18T15:41:21.582Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/35/2525f90c972d8aafc39784a8c00244eeee8e8221b26cbc576748ee9dc1cd/zopfli-0.2.3.post1-cp313-cp313-win32.whl", hash = "sha256:71390dbd3fbf6ebea9a5d85ffed8c26ee1453ee09248e9b88486e30e0397b775", size = 82742, upload-time = "2024-10-18T15:41:23.362Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/c6/49b27570923956d52d37363e8f5df3a31a61bd7719bb8718527a9df3ae5f/zopfli-0.2.3.post1-cp313-cp313-win_amd64.whl", hash = "sha256:a86eb88e06bd87e1fff31dac878965c26b0c26db59ddcf78bb0379a954b120de", size = 99408, upload-time = "2024-10-18T15:41:24.377Z" },
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user