Compare commits
10 Commits
f359a64218
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
a56402c2cc
|
|||
|
f2015e2c71
|
|||
|
e43c534ceb
|
|||
|
66f9304cc6
|
|||
|
65b9c8e70e
|
|||
|
8865a11d67
|
|||
|
8b256bda98
|
|||
|
8136740105
|
|||
|
171cfed7e3
|
|||
|
56257e85d6
|
1
.gitignore
vendored
@@ -12,3 +12,4 @@ eicar.com
|
||||
*.pp
|
||||
*.mod
|
||||
*.log
|
||||
scratch/
|
||||
@@ -41,6 +41,7 @@ or give me access to your servers.
|
||||
- [tmux](#tmux)
|
||||
- [bash](#bash)
|
||||
- [Bulk File/Folder Renaming](#bulk-filefolder-renaming)
|
||||
- [Escaping a Stuck SSH Terminal](#escaping-a-stuck-ssh-terminal)
|
||||
- [SSH Setup](#ssh-setup)
|
||||
- [Git GPG Commit Signing](#git-gpg-commit-signing)
|
||||
- [Important Dates and Times](#important-dates-and-times)
|
||||
@@ -128,6 +129,10 @@ for change_dir in $(ls | grep 'podman_*'); do
|
||||
done
|
||||
```
|
||||
|
||||
### Escaping a Stuck SSH Terminal
|
||||
|
||||
Press the following keys: enter + ~ + .
|
||||
|
||||
## SSH Setup
|
||||
|
||||
Generate a key (password protect it!)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
- [Reeseapps vs Reeselink](#reeseapps-vs-reeselink)
|
||||
- [Reeselink Addresses](#reeselink-addresses)
|
||||
- [Reeseapps Addresses](#reeseapps-addresses)
|
||||
- [Converting Unifi Records to AWS Records](#converting-unifi-records-to-aws-records)
|
||||
|
||||
## Reeseapps vs Reeselink
|
||||
|
||||
@@ -28,3 +29,14 @@ aws route53 change-resource-record-sets --hosted-zone-id $(cat active/aws_route5
|
||||
```bash
|
||||
aws route53 change-resource-record-sets --hosted-zone-id $(cat active/aws_route53/secrets/reeseapps-zoneid) --change-batch file://active/aws_route53/secrets/reeseapps.json
|
||||
```
|
||||
|
||||
## Converting Unifi Records to AWS Records
|
||||
|
||||
The script `unifi_to_aws.py` will create a file at
|
||||
`secrets/unifi_reeselink_records.json` which contains all `reeselink.com`
|
||||
domains in the unifi server converted to AWS route53 batch format. Simply run
|
||||
the script and then use that file to update reeselink.com records.
|
||||
|
||||
```python
|
||||
python active/aws_route53/unifi_to_aws.py
|
||||
```
|
||||
|
||||
76
active/aws_route53/sync_unifi_records.sh
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
|
||||
# --- Configuration ---
|
||||
PYTHON_SCRIPT="active/aws_route53/unifi_to_aws.py"
|
||||
ZONE_ID_FILE="active/aws_route53/secrets/reeselink-zoneid"
|
||||
RECORDS_FILE="active/aws_route53/secrets/unifi_reeselink_records.json"
|
||||
|
||||
# --- Colors for logging ---
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# --- Logging Function ---
|
||||
log() {
|
||||
echo -e "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
error_exit() {
|
||||
echo -e "[$(date +'%Y-%m-%d %H:%M:%S')] ${RED}ERROR: $1${NC}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- 1. Pre-flight Checks ---
|
||||
log "${YELLOW}Starting Route53 update process...${NC}"
|
||||
|
||||
if [[ ! -f "$PYTHON_SCRIPT" ]]; then
|
||||
error_exit "Python script not found at $PYTHON_SCRIPT"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$ZONE_ID_FILE" ]]; then
|
||||
error_exit "Zone ID file not found at $ZONE_ID_FILE"
|
||||
fi
|
||||
|
||||
# --- 2. Run Python Script ---
|
||||
log "Running $PYTHON_SCRIPT to generate JSON records..."
|
||||
|
||||
# Execute the python script
|
||||
python "$PYTHON_SCRIPT"
|
||||
|
||||
# Check the exit code of the python script
|
||||
if [[ $? -eq 0 ]]; then
|
||||
log "${GREEN}Python script executed successfully.${NC}"
|
||||
else
|
||||
error_exit "Python script failed. Aborting AWS update to prevent corrupting DNS."
|
||||
fi
|
||||
|
||||
# Verify the output file actually exists after the python run
|
||||
if [[ ! -f "$RECORDS_FILE" ]]; then
|
||||
error_exit "Python script reported success, but $RECORDS_FILE was not found."
|
||||
fi
|
||||
|
||||
# --- 3. Update Route53 ---
|
||||
# Read the Zone ID from the secret file
|
||||
ZONE_ID=$(cat "$ZONE_ID_FILE" | tr -d '\n\r ')
|
||||
|
||||
if [[ -z "$ZONE_ID" ]]; then
|
||||
error_exit "Zone ID file is empty or could not be read."
|
||||
fi
|
||||
|
||||
log "Updating Route53 records for Zone ID: $ZONE_ID..."
|
||||
|
||||
# Run the AWS CLI command
|
||||
# Using file:// prefix as required by AWS CLI for local files
|
||||
aws route53 change-resource-record-sets \
|
||||
--hosted-zone-id "$ZONE_ID" \
|
||||
--change-batch "file://$RECORDS_FILE"
|
||||
|
||||
# Check the exit code of the AWS command
|
||||
if [[ $? -eq 0 ]]; then
|
||||
log "${GREEN}Route53 records updated successfully!${NC}"
|
||||
else
|
||||
error_exit "AWS CLI command failed. Check your AWS credentials and JSON formatting."
|
||||
fi
|
||||
|
||||
log "${GREEN}Process complete.${NC}"
|
||||
113
active/aws_route53/unifi_to_aws.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import requests
|
||||
|
||||
# Configuration
|
||||
API_KEY = os.environ.get("API_KEY")
|
||||
URL_DEVICES = "https://10.1.0.1/proxy/network/v2/api/site/default/static-dns/devices"
|
||||
URL_POLICIES = "https://10.1.0.1/proxy/network/integration/v1/sites/88f7af54-98f8-306a-a1c7-c9349722b1f6/dns/policies"
|
||||
OUTPUT_FILE = "active/aws_route53/secrets/unifi_reeselink_records.json"
|
||||
|
||||
ALLOWED_DOMAIN = "reeselink.com"
|
||||
FIXED_TTL = 60
|
||||
|
||||
# Headers
|
||||
headers = {"Accept": "application/json", "X-API-Key": API_KEY}
|
||||
|
||||
|
||||
def fetch_json(url: str) -> Any:
|
||||
"""Helper to perform the GET request and return parsed JSON."""
|
||||
response = requests.get(
|
||||
url,
|
||||
headers=headers,
|
||||
verify=False, # -k: Don't verify SSL certificate
|
||||
allow_redirects=True, # -L: Follow redirects
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f"Error: Received status code {response.status_code} from {url}")
|
||||
print(f"Response: {response.text}")
|
||||
sys.exit(1)
|
||||
return response.json()
|
||||
|
||||
|
||||
def main():
|
||||
all_changes: List[Dict[str, Any]] = []
|
||||
|
||||
# 1. Process Devices API
|
||||
devices_data = fetch_json(URL_DEVICES)
|
||||
devices_count = 0
|
||||
|
||||
# devices_data is expected to be a list: [{hostname: ..., ip_address: ...}, ...]
|
||||
for device in devices_data:
|
||||
hostname = device.get("hostname", "")
|
||||
ip = device.get("ip_address", "")
|
||||
|
||||
if hostname.endswith(ALLOWED_DOMAIN):
|
||||
all_changes.append(
|
||||
{
|
||||
"Action": "UPSERT",
|
||||
"ResourceRecordSet": {
|
||||
"Name": hostname,
|
||||
"Type": "A",
|
||||
"TTL": FIXED_TTL,
|
||||
"ResourceRecords": [{"Value": ip}],
|
||||
},
|
||||
}
|
||||
)
|
||||
devices_count += 1
|
||||
|
||||
# 2. Process Policies API
|
||||
policies_response = fetch_json(URL_POLICIES)
|
||||
policies_count = 0
|
||||
|
||||
# policies_response is expected to be a dict: {"data": [{domain: ..., ipv4Address: ...}, ...]}
|
||||
policies_list = policies_response.get("data", [])
|
||||
for policy in policies_list:
|
||||
domain = policy.get("domain", "")
|
||||
ip = policy.get("ipv4Address", "")
|
||||
|
||||
if domain.endswith(ALLOWED_DOMAIN):
|
||||
all_changes.append(
|
||||
{
|
||||
"Action": "UPSERT",
|
||||
"ResourceRecordSet": {
|
||||
"Name": domain,
|
||||
"Type": "A",
|
||||
"TTL": FIXED_TTL,
|
||||
"ResourceRecords": [{"Value": ip}],
|
||||
},
|
||||
}
|
||||
)
|
||||
policies_count += 1
|
||||
|
||||
# Construct Final AWS Payload
|
||||
final_payload = {
|
||||
"Comment": "Combined records from Unifi devices and policies",
|
||||
"Changes": all_changes,
|
||||
}
|
||||
|
||||
# Write to file
|
||||
try:
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True)
|
||||
with open(OUTPUT_FILE, "w") as f:
|
||||
json.dump(final_payload, f, indent=4)
|
||||
except Exception as e:
|
||||
print(f"Error writing to file: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Print Summary
|
||||
print(f"Successfully processed records:")
|
||||
print(f" - devices: {devices_count}")
|
||||
print(f" - policies: {policies_count}")
|
||||
print(f"Total records in file: {len(all_changes)}")
|
||||
print(f"Saved to {OUTPUT_FILE}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Suppress InsecureRequestWarning for verify=False
|
||||
requests.packages.urllib3.disable_warnings() # type: ignore
|
||||
main()
|
||||
@@ -154,7 +154,7 @@ curl -L -X POST 'https://aipi.reeseapps.com/v1/chat/completions' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer sk-1234' \
|
||||
-d '{
|
||||
"model": "gpt-4o-mini", # 👈 REPLACE with 'public model name' for any db-model
|
||||
"model": "driveripper/think",
|
||||
"messages": [
|
||||
{
|
||||
"content": "Hey, how's it going",
|
||||
|
||||
@@ -70,11 +70,6 @@ active/container_caddy/install_caddy_proxy.yaml
|
||||
ansible-playbook \
|
||||
-i ansible/inventory.yaml \
|
||||
active/container_caddy/install_caddy_deskwork.yaml
|
||||
|
||||
# Toybox (AI) Proxy
|
||||
ansible-playbook \
|
||||
-i ansible/inventory.yaml \
|
||||
active/container_caddy/install_caddy_toybox.yaml
|
||||
```
|
||||
|
||||
See ansible playbook [install_caddy.yaml](/active/container_caddy/install_caddy.yaml)
|
||||
|
||||
@@ -58,7 +58,7 @@ Now you can install the DDNS service with something like:
|
||||
```bash
|
||||
ansible-playbook \
|
||||
-i ansible/inventory.yaml \
|
||||
-l proxy \
|
||||
-l proxy-root \
|
||||
active/container_ddns/install_ddns.yaml
|
||||
```
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Compose
|
||||
|
||||
Put your compose.yaml here.
|
||||
@@ -1,15 +1,22 @@
|
||||
services:
|
||||
litellm:
|
||||
image: docker.litellm.ai/berriai/litellm:main-latest
|
||||
image: docker.litellm.ai/berriai/litellm:main-stable
|
||||
#########################################
|
||||
## Uncomment these lines to start proxy with a config.yaml file ##
|
||||
# volumes:
|
||||
# - ./config.yaml:/app/config.yaml
|
||||
# command:
|
||||
# - "--config=/app/config.yaml"
|
||||
##############################################
|
||||
ports:
|
||||
- 4000:4000
|
||||
env_file: /home/ai/litellm.env
|
||||
- "4000:4000" # Map the container port to the host, change the host port if necessary
|
||||
environment:
|
||||
DATABASE_URL: "postgresql://llmproxy:dbpassword9090@host.containers.internal:5432/litellm"
|
||||
STORE_MODEL_IN_DB: "True"
|
||||
restart: unless-stopped
|
||||
DATABASE_URL: "postgresql://llmproxy:dbpassword9090@db:5432/litellm"
|
||||
STORE_MODEL_IN_DB: "True" # allows adding models to proxy via UI
|
||||
env_file:
|
||||
- ../secrets/litellm.env # Load local .env file
|
||||
depends_on:
|
||||
- litellm-db # Indicates that this service depends on the 'litellm-db' service, ensuring 'litellm-db' starts first
|
||||
- db # Indicates that this service depends on the 'db' service, ensuring 'db' starts first
|
||||
healthcheck: # Defines the health check configuration for the container
|
||||
test:
|
||||
- CMD-SHELL
|
||||
@@ -19,9 +26,10 @@ services:
|
||||
retries: 3 # Retry up to 3 times if health check fails
|
||||
start_period: 40s # Wait 40 seconds after container start before beginning health checks
|
||||
|
||||
litellm-db:
|
||||
db:
|
||||
image: docker.io/postgres:16
|
||||
restart: always
|
||||
container_name: litellm_db
|
||||
environment:
|
||||
POSTGRES_DB: litellm
|
||||
POSTGRES_USER: llmproxy
|
||||
@@ -29,9 +37,26 @@ services:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- litellm_postgres_data:/var/lib/postgresql/data:z
|
||||
- postgres_data:/var/lib/postgresql/data # Persists Postgres data across container restarts
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d litellm -U llmproxy"]
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
prometheus:
|
||||
image: docker.io/prom/prometheus
|
||||
volumes:
|
||||
- prometheus_data:/prometheus
|
||||
- ../seccrets/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
ports:
|
||||
- "9090:9090"
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
- "--storage.tsdb.path=/prometheus"
|
||||
- "--storage.tsdb.retention.time=15d"
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
prometheus_data:
|
||||
postgres_data:
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
# General settings
|
||||
|
||||
general_settings:
|
||||
request_timeout: 600
|
||||
|
||||
# Models
|
||||
model_list:
|
||||
# Qwen3.5-35B variants
|
||||
- model_name: qwen3.5-35b-think-general
|
||||
litellm_params:
|
||||
model: openai/qwen3.5-35b-a3b
|
||||
api_base: https://llama-cpp.reeselink.com
|
||||
api_key: none
|
||||
temperature: 1.0
|
||||
top_p: 0.95
|
||||
presence_penalty: 1.5
|
||||
extra_body:
|
||||
top_k: 20
|
||||
min_p: 0.0
|
||||
repetition_penalty: 1.0
|
||||
chat_template_kwargs:
|
||||
enable_thinking: true
|
||||
|
||||
- model_name: qwen3.5-35b-think-code
|
||||
litellm_params:
|
||||
model: openai/qwen3.5-35b-a3b
|
||||
api_base: https://llama-cpp.reeselink.com
|
||||
api_key: none
|
||||
temperature: 0.6
|
||||
top_p: 0.95
|
||||
presence_penalty: 0.0
|
||||
extra_body:
|
||||
top_k: 20
|
||||
min_p: 0.0
|
||||
repetition_penalty: 1.0
|
||||
chat_template_kwargs:
|
||||
enable_thinking: true
|
||||
|
||||
- model_name: qwen3.5-35b-instruct-general
|
||||
litellm_params:
|
||||
model: openai/qwen3.5-35b-a3b
|
||||
api_base: https://llama-cpp.reeselink.com
|
||||
api_key: none
|
||||
temperature: 0.7
|
||||
top_p: 0.8
|
||||
presence_penalty: 1.5
|
||||
extra_body:
|
||||
top_k: 20
|
||||
min_p: 0.0
|
||||
repetition_penalty: 1.0
|
||||
chat_template_kwargs:
|
||||
enable_thinking: false
|
||||
|
||||
- model_name: qwen3.5-35b-instruct-reasoning
|
||||
litellm_params:
|
||||
model: openai/qwen3.5-35b-a3b
|
||||
api_base: https://llama-cpp.reeselink.com
|
||||
api_key: none
|
||||
temperature: 1.0
|
||||
top_p: 0.95
|
||||
presence_penalty: 1.5
|
||||
extra_body:
|
||||
top_k: 20
|
||||
min_p: 0.0
|
||||
repetition_penalty: 1.0
|
||||
chat_template_kwargs:
|
||||
enable_thinking: false
|
||||
@@ -9,9 +9,8 @@
|
||||
- [Convert litellm compose spec to quadlets](#convert-litellm-compose-spec-to-quadlets)
|
||||
- [Create the litellm.env file](#create-the-litellmenv-file)
|
||||
- [Start and enable your systemd quadlet](#start-and-enable-your-systemd-quadlet)
|
||||
- [Install via Ansible](#install-via-ansible)
|
||||
- [Expose litellm](#expose-litellm)
|
||||
- [Using LiteLLM](#using-litellm)
|
||||
- [Adding Models](#adding-models)
|
||||
- [Testing Models](#testing-models)
|
||||
- [Backup litellm](#backup-litellm)
|
||||
- [Upgrade litellm](#upgrade-litellm)
|
||||
@@ -110,63 +109,28 @@ journalctl --user -u litellm -f
|
||||
systemctl --user enable --now podman-auto-update.timer
|
||||
```
|
||||
|
||||
### Install via Ansible
|
||||
|
||||
Preview changes with a dry run:
|
||||
|
||||
```bash
|
||||
ansible-playbook -i ansible/inventory.yaml active/container_litellm/playbook.yml --check --diff
|
||||
```
|
||||
|
||||
Run the playbook from the Homelab root:
|
||||
|
||||
```bash
|
||||
ansible-playbook -i ansible/inventory.yaml active/container_litellm/playbook.yml
|
||||
```
|
||||
|
||||
This copies the quadlets, config, reloads the systemd user daemon, and starts both `litellm-db` and `litellm` services as the `ai` user.
|
||||
|
||||
### Expose litellm
|
||||
|
||||
1. If you need a domain, follow the [DDNS instructions](/active/container_ddns/ddns.md#install-a-new-ddns-service)
|
||||
2. For a web service, follow the [Caddy instructions](/active/container_caddy/caddy.md#adding-a-new-caddy-record)
|
||||
3. Finally, follow your OS's guide for opening ports via its firewall service.
|
||||
|
||||
## Using LiteLLM
|
||||
|
||||
### Adding Models
|
||||
|
||||
```json
|
||||
// qwen3.5-35b-a3b-thinking
|
||||
{
|
||||
"temperature": 1,
|
||||
"top_p": 0.95,
|
||||
"presence_penalty": 1.5,
|
||||
"extra_body": {
|
||||
"top_k": 20,
|
||||
"min_p": 0,
|
||||
"repetition_penalty": 1,
|
||||
"chat_template_kwargs": {
|
||||
"enable_thinking": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// qwen3.5-35b-a3b-coding
|
||||
{
|
||||
"temperature": 0.6,
|
||||
"top_p": 0.95,
|
||||
"presence_penalty": 0,
|
||||
"extra_body": {
|
||||
"top_k": 20,
|
||||
"min_p": 0,
|
||||
"repetition_penalty": 1,
|
||||
"chat_template_kwargs": {
|
||||
"enable_thinking": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// qwen3.5-35b-a3b-instruct
|
||||
{
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.8,
|
||||
"presence_penalty": 1.5,
|
||||
"extra_body": {
|
||||
"top_k": 20,
|
||||
"min_p": 0,
|
||||
"repetition_penalty": 1,
|
||||
"chat_template_kwargs": {
|
||||
"enable_thinking": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Models
|
||||
|
||||
```bash
|
||||
@@ -191,7 +155,7 @@ curl -L -X POST 'https://aipi.reeseapps.com/v1/chat/completions' \
|
||||
|
||||
## Backup litellm
|
||||
|
||||
Follow the [Borg Backup instructions](/active/systemd_borg/borg.md#set-up-a-client-for-backup)
|
||||
Follow the [Borg Backup instructions](/active/software_borg/borg.md#set-up-a-client-for-backup)
|
||||
|
||||
## Upgrade litellm
|
||||
|
||||
|
||||
86
active/container_litellm/playbook.yml
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
- name: Install and start LiteLLM quadlets for ai user
|
||||
hosts: ai-ai
|
||||
remote_user: ai
|
||||
|
||||
vars:
|
||||
ai_user: ai
|
||||
quadlets_dir: "/home/{{ ai_user }}/.config/containers/systemd"
|
||||
|
||||
tasks:
|
||||
- name: Ensure ai user home directories exist
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ ai_user }}"
|
||||
group: "{{ ai_user }}"
|
||||
mode: "0755"
|
||||
loop:
|
||||
- "{{ quadlets_dir }}"
|
||||
|
||||
- name: Copy litellm container pod
|
||||
ansible.builtin.copy:
|
||||
src: quadlets/litellm.pod
|
||||
dest: "{{ quadlets_dir }}/litellm.pod"
|
||||
owner: "{{ ai_user }}"
|
||||
group: "{{ ai_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Copy litellm container quadlet
|
||||
ansible.builtin.copy:
|
||||
src: quadlets/litellm-web.container
|
||||
dest: "{{ quadlets_dir }}/litellm-web.container"
|
||||
owner: "{{ ai_user }}"
|
||||
group: "{{ ai_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Copy litellm-db container quadlet
|
||||
ansible.builtin.copy:
|
||||
src: quadlets/litellm-db.container
|
||||
dest: "{{ quadlets_dir }}/litellm-db.container"
|
||||
owner: "{{ ai_user }}"
|
||||
group: "{{ ai_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Copy prometheus container quadlet
|
||||
ansible.builtin.copy:
|
||||
src: quadlets/litellm-prometheus.container
|
||||
dest: "{{ quadlets_dir }}/litellm-prometheus.container"
|
||||
owner: "{{ ai_user }}"
|
||||
group: "{{ ai_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Copy prometheus config
|
||||
ansible.builtin.copy:
|
||||
src: secrets/litellm-prometheus.yaml
|
||||
dest: "/home/{{ ai_user }}/litellm-prometheus.yaml"
|
||||
owner: "{{ ai_user }}"
|
||||
group: "{{ ai_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Copy litellm.env file
|
||||
ansible.builtin.copy:
|
||||
src: secrets/litellm.env
|
||||
dest: "/home/{{ ai_user }}/litellm.env"
|
||||
owner: "{{ ai_user }}"
|
||||
group: "{{ ai_user }}"
|
||||
mode: "0600"
|
||||
|
||||
- name: Copy litellm-config.yaml
|
||||
ansible.builtin.copy:
|
||||
src: secrets/litellm-config.yaml
|
||||
dest: "/home/{{ ai_user }}/litellm-config.yaml"
|
||||
owner: "{{ ai_user }}"
|
||||
group: "{{ ai_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Reload systemd user daemon
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
scope: user
|
||||
|
||||
- name: Restart litellm pod
|
||||
ansible.builtin.systemd:
|
||||
name: litellm-pod
|
||||
state: restarted
|
||||
scope: user
|
||||
@@ -1,12 +1,13 @@
|
||||
[Container]
|
||||
Pod=litellm.pod
|
||||
ContainerName=litellm-db
|
||||
Environment=POSTGRES_DB=litellm POSTGRES_USER=llmproxy POSTGRES_PASSWORD=dbpassword9090
|
||||
HealthCmd='pg_isready -d litellm -U llmproxy'
|
||||
HealthInterval=1s
|
||||
HealthRetries=10
|
||||
HealthTimeout=5s
|
||||
Image=docker.io/postgres:16
|
||||
PublishPort=5432:5432
|
||||
Volume=litellm_postgres_data:/var/lib/postgresql/data:z
|
||||
Volume=litellm_postgres_data:/var/lib/postgresql/data
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[Container]
|
||||
Pod=litellm.pod
|
||||
ContainerName=litellm-prom
|
||||
Exec='--config.file=/etc/prometheus/prometheus.yml' '--storage.tsdb.path=/prometheus' '--storage.tsdb.retention.time=15d'
|
||||
Image=docker.io/prom/prometheus
|
||||
Volume=litellm_prometheus_data:/prometheus
|
||||
Volume=/home/ai/litellm-prometheus.yaml:/etc/prometheus/prometheus.yml:z
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -2,18 +2,18 @@
|
||||
Requires=litellm-db.service
|
||||
|
||||
[Container]
|
||||
Environment=DATABASE_URL=postgresql://llmproxy:dbpassword9090@host.containers.internal:5432/litellm STORE_MODEL_IN_DB=True
|
||||
Pod=litellm.pod
|
||||
ContainerName=litellm-web
|
||||
Environment=DATABASE_URL=postgresql://llmproxy:dbpassword9090@localhost:5432/litellm STORE_MODEL_IN_DB=True
|
||||
EnvironmentFile=/home/ai/litellm.env
|
||||
HealthCmd="python3 -c \"import urllib.request; urllib.request.urlopen('http://localhost:4000/health/liveliness')\""
|
||||
HealthInterval=30s
|
||||
HealthRetries=3
|
||||
HealthStartPeriod=40s
|
||||
HealthTimeout=10s
|
||||
Image=docker.litellm.ai/berriai/litellm:main-latest
|
||||
PublishPort=4000:4000
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
Image=ghcr.io/berriai/litellm-database:v1.83.14-stable.patch.3
|
||||
Volume=/home/ai/litellm-config.yaml:/app/config.yaml:z
|
||||
Exec=--config=/app/config.yaml
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
3
active/container_litellm/quadlets/litellm.pod
Normal file
@@ -0,0 +1,3 @@
|
||||
[Pod]
|
||||
# litellm web interface
|
||||
PublishPort=4000:4000/tcp
|
||||
@@ -3,6 +3,7 @@
|
||||
- [Driveripper](#driveripper)
|
||||
- [General Principles](#general-principles)
|
||||
- [Important Locations](#important-locations)
|
||||
- [Backups](#backups)
|
||||
- [Monitoring Scripts](#monitoring-scripts)
|
||||
- [Quick Ansible Commands](#quick-ansible-commands)
|
||||
- [Quickstart VM](#quickstart-vm)
|
||||
@@ -27,6 +28,28 @@
|
||||
- `/etc/luks-keys`: luks keys
|
||||
- `/usr/local/scripts`: admin scripts
|
||||
|
||||
## Backups
|
||||
|
||||
```bash
|
||||
# smb
|
||||
rsync -av --progress \
|
||||
--exclude .snapshots \
|
||||
/srv/smb/ \
|
||||
/srv/backup/smb/
|
||||
|
||||
# archive
|
||||
rsync -av --progress \
|
||||
--exclude .snapshots \
|
||||
/srv/archive/ \
|
||||
/srv/backup/archive/
|
||||
|
||||
# vm
|
||||
rsync -av --progress \
|
||||
--exclude .snapshots \
|
||||
/srv/vm/ \
|
||||
/srv/backup/vm/
|
||||
```
|
||||
|
||||
## Monitoring Scripts
|
||||
|
||||
```bash
|
||||
@@ -196,7 +219,7 @@ Retired 12-19-2025
|
||||
|
||||

|
||||
|
||||
### Sending emails
|
||||
## Sending emails
|
||||
|
||||
```bash
|
||||
# s-nail is mailx
|
||||
|
||||
BIN
active/device_esphome/beep.wav
Normal file
@@ -8,6 +8,7 @@
|
||||
- [Adding a New Device](#adding-a-new-device)
|
||||
- [Controlling Home Assistant](#controlling-home-assistant)
|
||||
- [Configuration Sections](#configuration-sections)
|
||||
- [Symbols](#symbols)
|
||||
- [esphome](#esphome)
|
||||
- [esp32](#esp32-1)
|
||||
- [logger](#logger)
|
||||
@@ -63,7 +64,8 @@ uv venv
|
||||
uv pip install esphome
|
||||
source .venv/bin/activate
|
||||
|
||||
esphome run m5stack-atom-echo.yaml
|
||||
# grep for debug logs only (helpful for filtering noise)
|
||||
esphome run tab1.yaml | grep -E '.*[\[D\]]'
|
||||
```
|
||||
|
||||
## Adding a New Device
|
||||
@@ -78,6 +80,13 @@ esphome run m5stack-atom-echo.yaml
|
||||
|
||||
<https://esphome.io/components/>
|
||||
|
||||
## Symbols
|
||||
|
||||
You can display the embedded symbols among the text by their codepoint address
|
||||
preceded by \u. For example: \uF00C :
|
||||
|
||||

|
||||
|
||||
### esphome
|
||||
|
||||
### esp32
|
||||
@@ -145,6 +154,22 @@ data:
|
||||
media_content_id: "media-source://media_source/local/wake_word_triggered.wav"
|
||||
```
|
||||
|
||||
Playing arbitrary sound:
|
||||
|
||||
```yaml
|
||||
audio_file:
|
||||
- id: beep_sound
|
||||
file: "beep.wav"
|
||||
|
||||
media_source:
|
||||
- platform: audio_file
|
||||
id: file_source
|
||||
|
||||
- media_player.speaker.play_on_device_media_file:
|
||||
media_file: beep_sound
|
||||
announcement: true
|
||||
```
|
||||
|
||||
### voice assistant
|
||||
|
||||
<https://esphome.io/components/voice_assistant/>
|
||||
|
||||
BIN
active/device_esphome/image.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
active/device_esphome/images/charging.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
active/device_esphome/images/charging.xcf
Normal file
|
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 431 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 430 KiB After Width: | Height: | Size: 109 KiB |
997
active/device_esphome/pyramid1.yaml
Normal file
@@ -0,0 +1,997 @@
|
||||
---
|
||||
substitutions:
|
||||
name: pyramid1
|
||||
friendly_name: Pyramid 1
|
||||
|
||||
# Casita images
|
||||
loading_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/loading_320_240.png
|
||||
idle_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/idle_320_240.png
|
||||
listening_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/listening_320_240.png
|
||||
thinking_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/thinking_320_240.png
|
||||
replying_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/replying_320_240.png
|
||||
error_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/error_320_240.png
|
||||
error_no_wifi_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-wifi.png
|
||||
error_no_ha_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-ha.png
|
||||
|
||||
# Fonts
|
||||
mdi_webfont_file: https://raw.githubusercontent.com/Templarian/MaterialDesign-Webfont/master/fonts/materialdesignicons-webfont.ttf
|
||||
|
||||
# Audio files
|
||||
wake_word_trigger_sound_file: wake_word_triggered.wav
|
||||
# timer_finished_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/timer_finished.flac
|
||||
# error_cloud_expired_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/error_cloud_expired.mp3
|
||||
|
||||
# Micro wake word models
|
||||
pick_pig: https://raw.githubusercontent.com/esphome/micro-wake-word-models/refs/heads/main/models/v2/experiments/hey_peppa_pig.json
|
||||
stop_model_file: https://github.com/kahrendt/microWakeWord/releases/download/stop/stop.json
|
||||
# Background colors
|
||||
loading_illustration_background_color: "000000"
|
||||
idle_illustration_background_color: "000000"
|
||||
listening_illustration_background_color: "FFFFFF"
|
||||
thinking_illustration_background_color: "FFFFFF"
|
||||
replying_illustration_background_color: "FFFFFF"
|
||||
error_illustration_background_color: "000000"
|
||||
|
||||
# Phases of the Voice Assistant
|
||||
# The voice assistant is ready to be triggered by a wake word
|
||||
voice_assist_idle_phase_id: "1"
|
||||
# The voice assistant is listening for a voice command
|
||||
voice_assist_listening_phase_id: "2"
|
||||
# The voice assistant is currently processing the command
|
||||
voice_assist_thinking_phase_id: "3"
|
||||
# The voice assistant is replying to the command
|
||||
voice_assist_replying_phase_id: "4"
|
||||
# The voice assistant is not ready
|
||||
voice_assist_not_ready_phase_id: "10"
|
||||
# The voice assistant encountered an error
|
||||
voice_assist_error_phase_id: "11"
|
||||
# Muted phase
|
||||
voice_assist_muted_phase_id: "12"
|
||||
# Finished timer phase
|
||||
voice_assist_timer_finished_phase_id: "20"
|
||||
|
||||
esphome:
|
||||
name: pyramid1
|
||||
friendly_name: Pyramid 1
|
||||
min_version: 2025.11.3
|
||||
on_boot:
|
||||
priority: 600
|
||||
then:
|
||||
- delay: 30s
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(init_in_progress);
|
||||
then:
|
||||
- lambda: id(init_in_progress) = false;
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
flash_size: 8MB
|
||||
cpu_frequency: 240MHz
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
api:
|
||||
encryption:
|
||||
key: "innoIL7I6ZfRekL58F65REjeYNLW1Hp/Q/Kv9SEjnNA="
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "22de00dcf5c2701a25d2fe719d596123"
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
ap:
|
||||
ssid: "Echo-Pyramid Fallback Hotspot"
|
||||
password: "uSTvJjVzweZp"
|
||||
|
||||
# Enable logging
|
||||
logger:
|
||||
level: INFO
|
||||
logs:
|
||||
sensor: WARN
|
||||
|
||||
captive_portal:
|
||||
|
||||
button:
|
||||
- platform: factory_reset
|
||||
id: factory_reset_btn
|
||||
internal: true
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
pin:
|
||||
number: GPIO41
|
||||
mode: INPUT_PULLUP
|
||||
inverted: true
|
||||
id: user_button
|
||||
internal: true
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at least 50ms
|
||||
- OFF for at least 50ms
|
||||
then:
|
||||
- switch.turn_off: timer_ringing
|
||||
- timing:
|
||||
- ON for at least 10s
|
||||
then:
|
||||
- button.press: factory_reset_btn
|
||||
|
||||
external_components:
|
||||
- source: github://m5stack/esphome-yaml/components
|
||||
components: [aw87559, si5351, lp5562, pyramidrgb, pyramidtouch]
|
||||
refresh: 0s
|
||||
|
||||
# I2C Bus Configuration
|
||||
i2c:
|
||||
- id: bsp_bus
|
||||
sda: GPIO45
|
||||
scl: GPIO0
|
||||
scan: true
|
||||
- id: ext_bus # used on atomic echo base
|
||||
sda: GPIO38
|
||||
scl: GPIO39
|
||||
|
||||
# Ehco Base GPIO Expander
|
||||
pi4ioe5v6408:
|
||||
- id: pi4ioe5v6408_hub
|
||||
i2c_id: ext_bus
|
||||
address: 0x43
|
||||
|
||||
aw87559:
|
||||
id: audio_amp
|
||||
i2c_id: ext_bus
|
||||
address: 0x5B
|
||||
|
||||
si5351:
|
||||
id: clock_gen
|
||||
i2c_id: ext_bus
|
||||
address: 0x60
|
||||
|
||||
# I2S Bus Configuration
|
||||
i2s_audio:
|
||||
- id: i2s_audio_bus
|
||||
i2s_lrclk_pin: GPIO8
|
||||
i2s_bclk_pin: GPIO6
|
||||
|
||||
spi:
|
||||
clk_pin: GPIO15
|
||||
mosi_pin: GPIO21
|
||||
# miso_pin is not used
|
||||
|
||||
audio_dac:
|
||||
- platform: es8311
|
||||
id: es8311_dac
|
||||
i2c_id: ext_bus
|
||||
bits_per_sample: 16bit
|
||||
sample_rate: 16000
|
||||
|
||||
audio_adc:
|
||||
- platform: es7210
|
||||
id: es7210_adc
|
||||
i2c_id: ext_bus
|
||||
address: 0x40
|
||||
bits_per_sample: 16bit
|
||||
sample_rate: 16000
|
||||
|
||||
microphone:
|
||||
- platform: i2s_audio
|
||||
id: i2s_mic
|
||||
sample_rate: 16000
|
||||
i2s_din_pin: GPIO5
|
||||
bits_per_sample: 16bit
|
||||
adc_type: external
|
||||
channel: stereo
|
||||
|
||||
speaker:
|
||||
- platform: i2s_audio
|
||||
id: i2s_speaker
|
||||
i2s_dout_pin: GPIO7
|
||||
dac_type: external
|
||||
bits_per_sample: 16bit
|
||||
sample_rate: 16000
|
||||
channel: mono
|
||||
audio_dac: es8311_dac
|
||||
|
||||
media_player:
|
||||
- platform: speaker
|
||||
name: "Echo Pyramid Player"
|
||||
id: echo_pyramid_player
|
||||
volume_min: 0.0
|
||||
volume_max: 1.0
|
||||
volume_initial: 0.10
|
||||
buffer_size: 6000
|
||||
announcement_pipeline:
|
||||
speaker: i2s_speaker
|
||||
format: WAV
|
||||
# sample_rate: 48000
|
||||
# num_channels: 1
|
||||
codec_support_enabled: false
|
||||
files:
|
||||
- id: wake_word_triggered_sound
|
||||
file: ${wake_word_trigger_sound_file}
|
||||
# - id: timer_finished_sound
|
||||
# file: ${timer_finished_sound_file}
|
||||
# - id: error_cloud_expired
|
||||
# file: ${error_cloud_expired_file}
|
||||
on_state:
|
||||
- logger.log: "State updated!"
|
||||
on_play:
|
||||
- logger.log: "Playback started!"
|
||||
|
||||
on_announcement:
|
||||
- logger.log: "Announcing!"
|
||||
# Stop the wake word (mWW or VA) if the mic is capturing
|
||||
- if:
|
||||
condition:
|
||||
- microphone.is_capturing:
|
||||
then:
|
||||
- script.execute: stop_wake_word
|
||||
# Ensure VA stops before moving on
|
||||
- if:
|
||||
condition:
|
||||
- lambda: |-
|
||||
return id(wake_word_engine_location).current_option() == "In Home Assistant";
|
||||
then:
|
||||
- wait_until:
|
||||
- not:
|
||||
voice_assistant.is_running:
|
||||
# Since VA isn't running, this is user-intiated media playback. Draw the mute display
|
||||
- if:
|
||||
condition:
|
||||
not:
|
||||
voice_assistant.is_running:
|
||||
then:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
|
||||
- script.execute: draw_display
|
||||
on_idle:
|
||||
# Since VA isn't running, this is the end of user-intiated media playback. Restart the wake word.
|
||||
- if:
|
||||
condition:
|
||||
not:
|
||||
voice_assistant.is_running:
|
||||
then:
|
||||
- script.execute: start_wake_word
|
||||
- script.execute: set_idle_or_mute_phase
|
||||
- script.execute: draw_display
|
||||
|
||||
switch:
|
||||
# NS4150B
|
||||
- platform: gpio
|
||||
name: Speaker Enable
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe5v6408_hub
|
||||
number: 0
|
||||
mode:
|
||||
output: true
|
||||
icon: "mdi:volume-high"
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
|
||||
- platform: template
|
||||
name: Mute Microphone
|
||||
id: mute
|
||||
icon: "mdi:microphone-off"
|
||||
optimistic: true
|
||||
restore_mode: RESTORE_DEFAULT_OFF
|
||||
entity_category: config
|
||||
on_turn_off:
|
||||
- microphone.unmute:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
|
||||
|
||||
on_turn_on:
|
||||
- microphone.mute:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
|
||||
|
||||
- platform: template
|
||||
id: timer_ringing
|
||||
optimistic: true
|
||||
internal: true
|
||||
restore_mode: ALWAYS_OFF
|
||||
on_turn_off:
|
||||
# Turn off the repeat mode and disable the pause between playlist items
|
||||
- lambda: |-
|
||||
id(echo_pyramid_player)
|
||||
->make_call()
|
||||
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
id(echo_pyramid_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 0);
|
||||
# Stop playing the alarm
|
||||
- media_player.stop:
|
||||
announcement: true
|
||||
- script.execute: start_wake_word
|
||||
on_turn_on:
|
||||
- script.execute: stop_wake_word
|
||||
# Turn on the repeat mode and pause for 1000 ms between playlist items/repeats
|
||||
- lambda: |-
|
||||
id(echo_pyramid_player)
|
||||
->make_call()
|
||||
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
id(echo_pyramid_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 1000);
|
||||
# - media_player.speaker.play_on_device_media_file:
|
||||
# media_file: timer_finished_sound
|
||||
# announcement: true
|
||||
- delay: 15min
|
||||
- switch.turn_off: timer_ringing
|
||||
|
||||
select:
|
||||
- platform: template
|
||||
entity_category: config
|
||||
name: Wake word engine location
|
||||
id: wake_word_engine_location
|
||||
icon: "mdi:account-voice"
|
||||
optimistic: true
|
||||
restore_value: true
|
||||
options:
|
||||
- In Home Assistant
|
||||
- On device
|
||||
initial_option: On device
|
||||
on_value:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return !id(init_in_progress);
|
||||
then:
|
||||
- wait_until:
|
||||
lambda: return id(voice_assistant_phase) == ${voice_assist_muted_phase_id} || id(voice_assistant_phase) == ${voice_assist_idle_phase_id};
|
||||
- if:
|
||||
condition:
|
||||
lambda: return x == "In Home Assistant";
|
||||
then:
|
||||
- micro_wake_word.stop
|
||||
- delay: 500ms
|
||||
- if:
|
||||
condition:
|
||||
switch.is_off: mute
|
||||
then:
|
||||
- lambda: id(va).set_use_wake_word(true);
|
||||
- voice_assistant.start_continuous:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return x == "On device";
|
||||
then:
|
||||
- lambda: id(va).set_use_wake_word(false);
|
||||
- voice_assistant.stop
|
||||
- delay: 500ms
|
||||
- if:
|
||||
condition:
|
||||
switch.is_off: mute
|
||||
then:
|
||||
- micro_wake_word.start
|
||||
|
||||
- platform: template
|
||||
name: "Wake word sensitivity"
|
||||
optimistic: true
|
||||
initial_option: Slightly sensitive
|
||||
restore_value: true
|
||||
entity_category: config
|
||||
options:
|
||||
- Slightly sensitive
|
||||
- Moderately sensitive
|
||||
- Very sensitive
|
||||
on_value:
|
||||
# Sets specific wake word probabilities computed for each particular model
|
||||
# Note probability cutoffs are set as a quantized uint8 value, each comment has the corresponding floating point cutoff
|
||||
# False Accepts per Hour values are tested against all units and channels from the Dinner Party Corpus.
|
||||
# These cutoffs apply only to the specific models included in the firmware: okay_nabu@20241226.3, hey_jarvis@v2, hey_mycroft@v2
|
||||
lambda: |-
|
||||
if (x == "Slightly sensitive") {
|
||||
id(okay_nabu).set_probability_cutoff(217); // 0.85 -> 0.000 FAPH on DipCo (Manifest's default)
|
||||
id(hey_jarvis).set_probability_cutoff(247); // 0.97 -> 0.563 FAPH on DipCo (Manifest's default)
|
||||
id(hey_mycroft).set_probability_cutoff(253); // 0.99 -> 0.567 FAPH on DipCo
|
||||
} else if (x == "Moderately sensitive") {
|
||||
id(okay_nabu).set_probability_cutoff(176); // 0.69 -> 0.376 FAPH on DipCo
|
||||
id(hey_jarvis).set_probability_cutoff(235); // 0.92 -> 0.939 FAPH on DipCo
|
||||
id(hey_mycroft).set_probability_cutoff(242); // 0.95 -> 1.502 FAPH on DipCo (Manifest's default)
|
||||
} else if (x == "Very sensitive") {
|
||||
id(okay_nabu).set_probability_cutoff(143); // 0.56 -> 0.751 FAPH on DipCo
|
||||
id(hey_jarvis).set_probability_cutoff(212); // 0.83 -> 1.502 FAPH on DipCo
|
||||
id(hey_mycroft).set_probability_cutoff(237); // 0.93 -> 1.878 FAPH on DipCo
|
||||
}
|
||||
|
||||
micro_wake_word:
|
||||
id: mww
|
||||
microphone: i2s_mic
|
||||
models:
|
||||
- model: okay_nabu
|
||||
id: okay_nabu
|
||||
- model: hey_jarvis
|
||||
id: hey_jarvis
|
||||
- model: hey_mycroft
|
||||
id: hey_mycroft
|
||||
- model: https://raw.githubusercontent.com/esphome/micro-wake-word-models/refs/heads/main/models/v2/experiments/hey_peppa_pig.json
|
||||
id: hey_peppa_pig
|
||||
- model: ${stop_model_file}
|
||||
id: stop
|
||||
internal: true
|
||||
vad:
|
||||
on_wake_word_detected:
|
||||
- script.execute:
|
||||
id: play_sound
|
||||
priority: true
|
||||
sound_file: !lambda return id(wake_word_triggered_sound);
|
||||
|
||||
- wait_until:
|
||||
condition:
|
||||
- media_player.is_announcing:
|
||||
timeout: 0.5s
|
||||
# Announcement is finished and the I2S bus is free
|
||||
- wait_until:
|
||||
- and:
|
||||
- not:
|
||||
media_player.is_announcing:
|
||||
- not:
|
||||
speaker.is_playing:
|
||||
|
||||
- voice_assistant.start:
|
||||
wake_word: !lambda return wake_word;
|
||||
|
||||
voice_assistant:
|
||||
id: va
|
||||
microphone: i2s_mic
|
||||
media_player: echo_pyramid_player
|
||||
micro_wake_word: mww
|
||||
noise_suppression_level: 2
|
||||
auto_gain: 31dBFS
|
||||
volume_multiplier: 2.0
|
||||
on_listening:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_listening_phase_id};
|
||||
- script.execute: draw_display
|
||||
on_stt_vad_end:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id};
|
||||
- script.execute: draw_display
|
||||
on_tts_start:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id};
|
||||
- script.execute: draw_display
|
||||
on_end:
|
||||
# Wait a short amount of time to see if an announcement starts
|
||||
- wait_until:
|
||||
condition:
|
||||
- media_player.is_announcing:
|
||||
timeout: 0.5s
|
||||
# Announcement is finished and the I2S bus is free
|
||||
- wait_until:
|
||||
- and:
|
||||
- not:
|
||||
media_player.is_announcing:
|
||||
- not:
|
||||
speaker.is_playing:
|
||||
# Restart only mWW if enabled; streaming wake words automatically restart
|
||||
- if:
|
||||
condition:
|
||||
- lambda: |-
|
||||
return id(wake_word_engine_location).current_option() == "On device";
|
||||
then:
|
||||
- lambda: id(va).set_use_wake_word(false);
|
||||
- micro_wake_word.start:
|
||||
- script.execute: set_idle_or_mute_phase
|
||||
- script.execute: draw_display
|
||||
|
||||
on_error:
|
||||
# Only set the error phase if the error code is different than duplicate_wake_up_detected or stt-no-text-recognized
|
||||
# These two are ignored for a better user experience
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- lambda: return !id(init_in_progress);
|
||||
- lambda: return code != "duplicate_wake_up_detected";
|
||||
- lambda: return code != "stt-no-text-recognized";
|
||||
then:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id};
|
||||
- script.execute: draw_display
|
||||
- delay: 1s
|
||||
- if:
|
||||
condition:
|
||||
switch.is_off: mute
|
||||
then:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
|
||||
else:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
|
||||
# If the error code is cloud-auth-failed, serve a local audio file guiding the user.
|
||||
- if:
|
||||
condition:
|
||||
- lambda: return code == "cloud-auth-failed";
|
||||
then:
|
||||
# - script.execute:
|
||||
# id: play_sound
|
||||
# priority: true
|
||||
# sound_file: !lambda return id(error_cloud_expired);
|
||||
- script.execute: draw_display
|
||||
|
||||
on_client_connected:
|
||||
- lambda: id(init_in_progress) = false;
|
||||
- script.execute: start_wake_word
|
||||
- script.execute: set_idle_or_mute_phase
|
||||
- script.execute: draw_display
|
||||
|
||||
on_client_disconnected:
|
||||
- script.execute: stop_wake_word
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id};
|
||||
- script.execute: draw_display
|
||||
|
||||
on_timer_finished:
|
||||
- switch.turn_on: timer_ringing
|
||||
- wait_until:
|
||||
media_player.is_announcing:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_timer_finished_phase_id};
|
||||
|
||||
globals:
|
||||
- id: init_in_progress
|
||||
type: bool
|
||||
restore_value: false
|
||||
initial_value: "true"
|
||||
- id: voice_assistant_phase
|
||||
type: int
|
||||
restore_value: false
|
||||
initial_value: ${voice_assist_not_ready_phase_id}
|
||||
- id: current_volume
|
||||
type: float
|
||||
restore_value: true
|
||||
initial_value: "0.3"
|
||||
|
||||
sensor:
|
||||
- platform: pyramidtouch
|
||||
address: 0x1A
|
||||
i2c_id: ext_bus
|
||||
update_interval: 50ms
|
||||
publish_swipe_event: true
|
||||
swipe_timeout_ms: 500
|
||||
touch1:
|
||||
name: "Touch 1"
|
||||
touch2:
|
||||
name: "Touch 2"
|
||||
touch3:
|
||||
name: "Touch 3"
|
||||
touch4:
|
||||
name: "Touch 4"
|
||||
swipe_event:
|
||||
name: "Touch Swipe Event"
|
||||
entity_category: diagnostic
|
||||
on_value:
|
||||
then:
|
||||
- lambda: |-
|
||||
// Swipe codes:
|
||||
// 1 = Left Up (volume up)
|
||||
// 2 = Left Down (volume down)
|
||||
// 3 = Right Up (brightness up)
|
||||
// 4 = Right Down (brightness down)
|
||||
const float volume_step = 0.05f; // 5% volume per gesture
|
||||
const float brightness_step = 5.0f; // 5% brightness per gesture
|
||||
|
||||
const int ev = (int) x;
|
||||
|
||||
if (ev == 1 || ev == 2) {
|
||||
// Left side: control volume (0.0 - 1.0)
|
||||
float v = id(current_volume);
|
||||
if (ev == 1) {
|
||||
v = std::min(1.0f, v + volume_step);
|
||||
} else {
|
||||
v = std::max(0.0f, v - volume_step);
|
||||
}
|
||||
|
||||
auto call = id(echo_pyramid_player).make_call();
|
||||
call.set_volume(v);
|
||||
call.perform();
|
||||
|
||||
id(current_volume) = v;
|
||||
} else if (ev == 3 || ev == 4) {
|
||||
// Right side: control RGB brightness (0 - 100)
|
||||
float b = id(rgb_master_brightness).state;
|
||||
if (ev == 3) {
|
||||
b = std::min(100.0f, b + brightness_step);
|
||||
} else {
|
||||
b = std::max(0.0f, b - brightness_step);
|
||||
}
|
||||
|
||||
uint8_t b8 = (uint8_t) b;
|
||||
id(pyramid_rgb1).set_strip_brightness(1, b8);
|
||||
id(pyramid_rgb2).set_strip_brightness(2, b8);
|
||||
id(rgb_master_brightness).publish_state(b);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
lp5562:
|
||||
id: lp5562_led
|
||||
i2c_id: bsp_bus
|
||||
use_internal_clk: true
|
||||
# power_save_mode: true
|
||||
# high_pwm_freq: true
|
||||
# logarithmic_dimming: true
|
||||
white_current: 17.5
|
||||
|
||||
pyramidrgb:
|
||||
- id: pyramid_rgb1
|
||||
i2c_id: ext_bus
|
||||
address: 0x1A
|
||||
strip: 1
|
||||
brightness: 80
|
||||
- id: pyramid_rgb2
|
||||
i2c_id: ext_bus
|
||||
address: 0x1A
|
||||
strip: 2
|
||||
brightness: 80
|
||||
|
||||
number:
|
||||
# Master media player volume (0.0–1.0)
|
||||
- platform: template
|
||||
name: "Master Volume"
|
||||
id: master_volume
|
||||
icon: "mdi:volume-high"
|
||||
min_value: 0.0
|
||||
max_value: 0.4
|
||||
step: 0.01
|
||||
restore_value: true
|
||||
initial_value: 0.3
|
||||
optimistic: true
|
||||
set_action:
|
||||
- lambda: |-
|
||||
float v = x;
|
||||
auto call = id(echo_pyramid_player).make_call();
|
||||
call.set_volume(v);
|
||||
call.perform();
|
||||
id(current_volume) = v;
|
||||
|
||||
# Master RGB brightness (applies to both strips, 0–100%)
|
||||
- platform: template
|
||||
name: "RGB Master Brightness"
|
||||
id: rgb_master_brightness
|
||||
icon: "mdi:brightness-6"
|
||||
min_value: 0
|
||||
max_value: 100
|
||||
step: 1
|
||||
restore_value: true
|
||||
initial_value: 100
|
||||
optimistic: true
|
||||
set_action:
|
||||
- lambda: |-
|
||||
uint8_t b = (uint8_t) x;
|
||||
id(pyramid_rgb1).set_strip_brightness(1, b);
|
||||
id(pyramid_rgb2).set_strip_brightness(2, b);
|
||||
|
||||
output:
|
||||
- platform: lp5562
|
||||
id: lp5562_white_channel
|
||||
lp5562_id: lp5562_led
|
||||
channel: white
|
||||
- platform: pyramidrgb
|
||||
id: rgb1_ch0_red
|
||||
pyramidrgb_id: pyramid_rgb1
|
||||
channel: 0
|
||||
color: red
|
||||
- platform: pyramidrgb
|
||||
id: rgb1_ch0_green
|
||||
pyramidrgb_id: pyramid_rgb1
|
||||
channel: 0
|
||||
color: green
|
||||
- platform: pyramidrgb
|
||||
id: rgb1_ch0_blue
|
||||
pyramidrgb_id: pyramid_rgb1
|
||||
channel: 0
|
||||
color: blue
|
||||
# Strip 1, Channel 1 (Group 2)
|
||||
- platform: pyramidrgb
|
||||
id: rgb1_ch1_red
|
||||
pyramidrgb_id: pyramid_rgb1
|
||||
channel: 1
|
||||
color: red
|
||||
- platform: pyramidrgb
|
||||
id: rgb1_ch1_green
|
||||
pyramidrgb_id: pyramid_rgb1
|
||||
channel: 1
|
||||
color: green
|
||||
- platform: pyramidrgb
|
||||
id: rgb1_ch1_blue
|
||||
pyramidrgb_id: pyramid_rgb1
|
||||
channel: 1
|
||||
color: blue
|
||||
|
||||
# Strip 2, Channel 2 (Group 1)
|
||||
- platform: pyramidrgb
|
||||
id: rgb2_ch2_red
|
||||
pyramidrgb_id: pyramid_rgb2
|
||||
channel: 2
|
||||
color: red
|
||||
- platform: pyramidrgb
|
||||
id: rgb2_ch2_green
|
||||
pyramidrgb_id: pyramid_rgb2
|
||||
channel: 2
|
||||
color: green
|
||||
- platform: pyramidrgb
|
||||
id: rgb2_ch2_blue
|
||||
pyramidrgb_id: pyramid_rgb2
|
||||
channel: 2
|
||||
color: blue
|
||||
|
||||
# Strip 2, Channel 3 (Group 2)
|
||||
- platform: pyramidrgb
|
||||
id: rgb2_ch3_red
|
||||
pyramidrgb_id: pyramid_rgb2
|
||||
channel: 3
|
||||
color: red
|
||||
- platform: pyramidrgb
|
||||
id: rgb2_ch3_green
|
||||
pyramidrgb_id: pyramid_rgb2
|
||||
channel: 3
|
||||
color: green
|
||||
- platform: pyramidrgb
|
||||
id: rgb2_ch3_blue
|
||||
pyramidrgb_id: pyramid_rgb2
|
||||
channel: 3
|
||||
color: blue
|
||||
|
||||
light:
|
||||
- platform: monochromatic
|
||||
name: "LCD Backlight"
|
||||
output: lp5562_white_channel
|
||||
icon: "mdi:television"
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
- platform: rgb
|
||||
name: "Strip1 Group1"
|
||||
red: rgb1_ch0_red
|
||||
green: rgb1_ch0_green
|
||||
blue: rgb1_ch0_blue
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
|
||||
- platform: rgb
|
||||
name: "Strip1 Group2"
|
||||
red: rgb1_ch1_red
|
||||
green: rgb1_ch1_green
|
||||
blue: rgb1_ch1_blue
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
|
||||
- platform: rgb
|
||||
name: "Strip2 Group1"
|
||||
red: rgb2_ch2_red
|
||||
green: rgb2_ch2_green
|
||||
blue: rgb2_ch2_blue
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
|
||||
- platform: rgb
|
||||
name: "Strip2 Group2"
|
||||
red: rgb2_ch3_red
|
||||
green: rgb2_ch3_green
|
||||
blue: rgb2_ch3_blue
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
id: atoms3r_lcd
|
||||
model: ST7789V
|
||||
dc_pin: GPIO42
|
||||
reset_pin: GPIO48
|
||||
cs_pin: GPIO14
|
||||
data_rate: 40MHz
|
||||
dimensions:
|
||||
height: 128
|
||||
width: 128
|
||||
offset_width: 2
|
||||
offset_height: 1
|
||||
|
||||
invert_colors: true
|
||||
rotation: 180°
|
||||
pages:
|
||||
- id: idle_page
|
||||
lambda: |-
|
||||
it.fill(id(idle_color));
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_idle), ImageAlign::CENTER);
|
||||
- id: listening_page
|
||||
lambda: |-
|
||||
it.fill(id(listening_color));
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_listening), ImageAlign::CENTER);
|
||||
- id: thinking_page
|
||||
lambda: |-
|
||||
it.fill(id(thinking_color));
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_thinking), ImageAlign::CENTER);
|
||||
- id: replying_page
|
||||
lambda: |-
|
||||
it.fill(id(replying_color));
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_replying), ImageAlign::CENTER);
|
||||
- id: error_page
|
||||
lambda: |-
|
||||
it.fill(id(error_color));
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_error), ImageAlign::CENTER);
|
||||
- id: no_ha_page
|
||||
lambda: |-
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(error_no_ha), ImageAlign::CENTER);
|
||||
- id: no_wifi_page
|
||||
lambda: |-
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(error_no_wifi), ImageAlign::CENTER);
|
||||
- id: initializing_page
|
||||
lambda: |-
|
||||
it.fill(id(loading_color));
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_initializing), ImageAlign::CENTER);
|
||||
- id: muted_page
|
||||
lambda: |-
|
||||
it.fill(Color::BLACK);
|
||||
it.printf(0, 0, id(mdi_icon_128), Color::WHITE, "%s", "\U000F036D");
|
||||
|
||||
script:
|
||||
# Starts either mWW or the streaming wake word, depending on the configured location
|
||||
- id: start_wake_word
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- not:
|
||||
- voice_assistant.is_running:
|
||||
- lambda: |-
|
||||
return id(wake_word_engine_location).current_option() == "On device";
|
||||
then:
|
||||
- lambda: id(va).set_use_wake_word(false);
|
||||
- micro_wake_word.start:
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- not:
|
||||
- voice_assistant.is_running:
|
||||
- lambda: |-
|
||||
return id(wake_word_engine_location).current_option() == "In Home Assistant";
|
||||
then:
|
||||
- lambda: id(va).set_use_wake_word(true);
|
||||
- voice_assistant.start_continuous:
|
||||
# Stops either mWW or the streaming wake word, depending on the configured location
|
||||
- id: stop_wake_word
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: |-
|
||||
return id(wake_word_engine_location).current_option() == "In Home Assistant";
|
||||
then:
|
||||
- lambda: id(va).set_use_wake_word(false);
|
||||
- voice_assistant.stop:
|
||||
- if:
|
||||
condition:
|
||||
lambda: |-
|
||||
return id(wake_word_engine_location).current_option() == "On device";
|
||||
then:
|
||||
- micro_wake_word.stop:
|
||||
# Set the voice assistant phase to idle or muted, depending on if the software mute switch is activated
|
||||
- id: set_idle_or_mute_phase
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
switch.is_off: mute
|
||||
then:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
|
||||
else:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
|
||||
|
||||
- id: play_sound
|
||||
parameters:
|
||||
priority: bool
|
||||
sound_file: "audio::AudioFile*"
|
||||
then:
|
||||
- lambda: |-
|
||||
if (priority) {
|
||||
id(echo_pyramid_player)
|
||||
->make_call()
|
||||
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
}
|
||||
if ( (id(echo_pyramid_player).state != media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING ) || priority) {
|
||||
id(echo_pyramid_player)
|
||||
->play_file(sound_file, true, false);
|
||||
}
|
||||
|
||||
- id: draw_display
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return !id(init_in_progress);
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
wifi.connected:
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
api.connected:
|
||||
then:
|
||||
- lambda: |
|
||||
switch(id(voice_assistant_phase)) {
|
||||
case ${voice_assist_listening_phase_id}:
|
||||
id(atoms3r_lcd).show_page(listening_page);
|
||||
id(atoms3r_lcd).update();
|
||||
break;
|
||||
case ${voice_assist_thinking_phase_id}:
|
||||
id(atoms3r_lcd).show_page(thinking_page);
|
||||
id(atoms3r_lcd).update();
|
||||
break;
|
||||
case ${voice_assist_replying_phase_id}:
|
||||
id(atoms3r_lcd).show_page(replying_page);
|
||||
id(atoms3r_lcd).update();
|
||||
break;
|
||||
case ${voice_assist_error_phase_id}:
|
||||
id(atoms3r_lcd).show_page(error_page);
|
||||
id(atoms3r_lcd).update();
|
||||
break;
|
||||
case ${voice_assist_muted_phase_id}:
|
||||
id(atoms3r_lcd).show_page(muted_page);
|
||||
id(atoms3r_lcd).update();
|
||||
break;
|
||||
case ${voice_assist_not_ready_phase_id}:
|
||||
id(atoms3r_lcd).show_page(no_ha_page);
|
||||
id(atoms3r_lcd).update();
|
||||
break;
|
||||
default:
|
||||
id(atoms3r_lcd).show_page(idle_page);
|
||||
id(atoms3r_lcd).update();
|
||||
}
|
||||
else:
|
||||
- display.page.show: no_ha_page
|
||||
- component.update: atoms3r_lcd
|
||||
else:
|
||||
- display.page.show: no_wifi_page
|
||||
- component.update: atoms3r_lcd
|
||||
else:
|
||||
- display.page.show: initializing_page
|
||||
- component.update: atoms3r_lcd
|
||||
|
||||
image:
|
||||
- file: ${error_illustration_file}
|
||||
id: casita_error
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${idle_illustration_file}
|
||||
id: casita_idle
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${listening_illustration_file}
|
||||
id: casita_listening
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${thinking_illustration_file}
|
||||
id: casita_thinking
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${replying_illustration_file}
|
||||
id: casita_replying
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${loading_illustration_file}
|
||||
id: casita_initializing
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${error_no_wifi_illustration_file}
|
||||
id: error_no_wifi
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${error_no_ha_illustration_file}
|
||||
id: error_no_ha
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
|
||||
font:
|
||||
- file: ${mdi_webfont_file}
|
||||
id: mdi_icon_128
|
||||
size: 128
|
||||
bpp: 4
|
||||
glyphs:
|
||||
- "\U000F036D" # mdi:mic-mute
|
||||
|
||||
color:
|
||||
- id: idle_color
|
||||
hex: ${idle_illustration_background_color}
|
||||
- id: listening_color
|
||||
hex: ${listening_illustration_background_color}
|
||||
- id: thinking_color
|
||||
hex: ${thinking_illustration_background_color}
|
||||
- id: replying_color
|
||||
hex: ${replying_illustration_background_color}
|
||||
- id: loading_color
|
||||
hex: ${loading_illustration_background_color}
|
||||
- id: error_color
|
||||
hex: ${error_illustration_background_color}
|
||||
599
active/device_esphome/tab1.yaml
Normal file
@@ -0,0 +1,599 @@
|
||||
esphome:
|
||||
name: tab1
|
||||
friendly_name: M5Stack Tab5 1
|
||||
on_boot:
|
||||
# Set the charging icon to the correct state on boot
|
||||
- then:
|
||||
- logger.log: "Delaying backlight initialization"
|
||||
- delay: 2s
|
||||
- logger.log: "End delay"
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(charging).state;
|
||||
then:
|
||||
- lvgl.widget.show:
|
||||
id: charging_icon_widget
|
||||
else:
|
||||
- lvgl.widget.hide:
|
||||
id: charging_icon_widget
|
||||
|
||||
esp32:
|
||||
board: esp32-p4-evboard
|
||||
flash_size: 16MB
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
enable_idf_experimental_features: true
|
||||
|
||||
esp32_hosted:
|
||||
variant: esp32c6
|
||||
active_high: true
|
||||
clk_pin: GPIO12
|
||||
cmd_pin: GPIO13
|
||||
d0_pin: GPIO11
|
||||
d1_pin: GPIO10
|
||||
d2_pin: GPIO9
|
||||
d3_pin: GPIO8
|
||||
reset_pin: GPIO15
|
||||
slot: 1
|
||||
|
||||
logger:
|
||||
hardware_uart: USB_SERIAL_JTAG
|
||||
level: DEBUG
|
||||
|
||||
psram:
|
||||
mode: hex
|
||||
speed: 200MHz
|
||||
|
||||
api:
|
||||
|
||||
# Touchscreen support
|
||||
external_components:
|
||||
- source: github://pr#12075
|
||||
components: [st7123]
|
||||
refresh: 1h
|
||||
|
||||
ota:
|
||||
platform: esphome
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
fast_connect: true
|
||||
on_connect:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "IDLE"
|
||||
- select.set:
|
||||
id: dac_output
|
||||
option: "LINE1"
|
||||
- lvgl.label.update:
|
||||
id: lbl_ip
|
||||
text: !lambda return id(ip_addr).state;
|
||||
- lvgl.label.update:
|
||||
id: lbl_ap
|
||||
text: !lambda return id(ssid).state;
|
||||
on_disconnect:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "DISCONNECTED"
|
||||
|
||||
text_sensor:
|
||||
- platform: wifi_info
|
||||
ip_address:
|
||||
id: ip_addr
|
||||
name: Device IP Address
|
||||
address_0:
|
||||
name: Device IP Address 0
|
||||
address_1:
|
||||
name: Device IP Address 1
|
||||
address_2:
|
||||
name: Device IP Address 2
|
||||
address_3:
|
||||
name: Device IP Address 3
|
||||
address_4:
|
||||
name: Device IP Address 4
|
||||
ssid:
|
||||
id: ssid
|
||||
name: Device Connected SSID
|
||||
bssid:
|
||||
name: Device Connected BSSID
|
||||
mac_address:
|
||||
name: Device Mac Wifi Address
|
||||
scan_results:
|
||||
name: Device Latest Scan Results
|
||||
dns_address:
|
||||
name: Device DNS Address
|
||||
power_save_mode:
|
||||
name: Device Wifi Power Save Mode
|
||||
|
||||
time:
|
||||
- platform: sntp
|
||||
id: sntp_time
|
||||
timezone: America/New_York
|
||||
servers:
|
||||
- 0.pool.ntp.org
|
||||
- 1.pool.ntp.org
|
||||
- 2.pool.ntp.org
|
||||
|
||||
# wireguard:
|
||||
# address: !secret tab1_wg_ip
|
||||
# private_key: !secret tab1_wg_pk
|
||||
# peer_endpoint: !secret wg_host
|
||||
# peer_public_key: !secret wg_pubkey
|
||||
|
||||
# # Optional keepalive (disabled by default)
|
||||
# peer_persistent_keepalive: 25s
|
||||
|
||||
i2c:
|
||||
- id: bsp_bus
|
||||
sda: GPIO31
|
||||
scl: GPIO32
|
||||
frequency: 400kHz
|
||||
|
||||
pi4ioe5v6408:
|
||||
- id: pi4ioe1
|
||||
address: 0x43
|
||||
# 0: O - wifi_antenna_int_ext
|
||||
# 1: O - speaker_enable
|
||||
# 2: O - external_5v_power
|
||||
# 3: NC
|
||||
# 4: O - lcd reset
|
||||
# 5: O - touch panel reset
|
||||
# 6: O - camera reset
|
||||
# 7: I - headphone detect
|
||||
- id: pi4ioe2
|
||||
address: 0x44
|
||||
# 0: O - wifi_power
|
||||
# 1: NC
|
||||
# 2: NC
|
||||
# 3: O - usb_5v_power
|
||||
# 4: O - poweroff pulse
|
||||
# 5: O - quick charge enable (inverted)
|
||||
# 6: I - charging status
|
||||
# 7: O - charge enable
|
||||
|
||||
button:
|
||||
- platform: restart
|
||||
name: "Restart Tablet"
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
id: wifi_power
|
||||
name: "WiFi Power"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe2
|
||||
number: 0
|
||||
restore_mode: ALWAYS_ON
|
||||
- platform: gpio
|
||||
id: usb_5v_power
|
||||
name: "USB Power"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe2
|
||||
number: 3
|
||||
- platform: gpio
|
||||
id: quick_charge
|
||||
name: "Quick Charge"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe2
|
||||
number: 5
|
||||
inverted: true
|
||||
- platform: gpio
|
||||
id: charge_enable
|
||||
name: "Charge Enable"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe2
|
||||
number: 7
|
||||
restore_mode: ALWAYS_ON
|
||||
- platform: gpio
|
||||
id: wifi_antenna_int_ext
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 0
|
||||
- platform: gpio
|
||||
id: speaker_enable
|
||||
name: "Speaker Enable"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 1
|
||||
restore_mode: ALWAYS_ON
|
||||
- platform: gpio
|
||||
id: external_5v_power
|
||||
name: "External 5V Power"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 2
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
id: charging
|
||||
name: "Charging Status"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe2
|
||||
number: 6
|
||||
mode: INPUT_PULLDOWN
|
||||
on_state:
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(charging).state;
|
||||
then:
|
||||
- lvgl.widget.show:
|
||||
id: charging_icon_widget
|
||||
else:
|
||||
- lvgl.widget.hide:
|
||||
id: charging_icon_widget
|
||||
|
||||
- platform: gpio
|
||||
id: headphone_detect
|
||||
name: "Headphone Detect"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 7
|
||||
- platform: lvgl
|
||||
widget: volume_up_widget
|
||||
name: Volume Up Button
|
||||
on_press:
|
||||
then:
|
||||
- logger.log: "Button pressed"
|
||||
- media_player.volume_up:
|
||||
id: tab5_media_player
|
||||
- delay: 100ms
|
||||
- lvgl.label.update:
|
||||
id: lbl_volume
|
||||
text: !lambda return to_string(int(id(tab5_media_player).volume * 100));
|
||||
- light.turn_on:
|
||||
id: backlight
|
||||
brightness: !lambda |-
|
||||
float current_value = id(backlight).current_values.get_brightness();
|
||||
return current_value < 0.6 ? 0.6 : current_value + 0.2;
|
||||
- platform: lvgl
|
||||
widget: volume_down_widget
|
||||
name: Volume Down Button
|
||||
on_press:
|
||||
then:
|
||||
- logger.log: "Button pressed"
|
||||
- media_player.volume_down:
|
||||
id: tab5_media_player
|
||||
- lvgl.label.update:
|
||||
id: lbl_volume
|
||||
text: !lambda return to_string(int(id(tab5_media_player).volume * 100));
|
||||
- light.turn_on:
|
||||
id: backlight
|
||||
brightness: !lambda |-
|
||||
float current_value = id(backlight).current_values.get_brightness();
|
||||
return current_value < 0.6 ? 0.6 : current_value - 0.2;
|
||||
|
||||
sensor:
|
||||
- platform: ina226
|
||||
address: 0x41
|
||||
adc_averaging: 16
|
||||
max_current: 8.192A
|
||||
shunt_resistance: 0.005ohm
|
||||
bus_voltage:
|
||||
id: battery_voltage
|
||||
name: "Battery Voltage"
|
||||
current:
|
||||
id: battery_current
|
||||
name: "Battery Current"
|
||||
# Positive means discharging
|
||||
# Negative means charging
|
||||
|
||||
# Tab5 built-in battery discharges from full (8.23 V) to shutdown threshold (6.0 V)
|
||||
- platform: template
|
||||
name: "Battery Percentage"
|
||||
lambda: |-
|
||||
float voltage = id(battery_voltage).state;
|
||||
// Adjust these values based on your battery's actual min/max voltage
|
||||
float min_voltage = 6.75; // Discharged voltage
|
||||
float max_voltage = 8.2; // Fully charged voltage
|
||||
float percentage = (voltage - min_voltage) / (max_voltage - min_voltage) * 100.0;
|
||||
if (percentage > 100.0) return 100.0;
|
||||
if (percentage < 0.0) return 0.0;
|
||||
return percentage;
|
||||
update_interval: 60s
|
||||
unit_of_measurement: "%"
|
||||
accuracy_decimals: 1
|
||||
id: battery_percent
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.label.update:
|
||||
id: lbl_battery
|
||||
text:
|
||||
format: "Battery: %.1f%%"
|
||||
args: ["id(battery_percent).state"]
|
||||
|
||||
touchscreen:
|
||||
- platform: st7123
|
||||
i2c_id: bsp_bus
|
||||
interrupt_pin: GPIO23
|
||||
display: lcd
|
||||
update_interval: never
|
||||
reset_pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 5
|
||||
calibration:
|
||||
x_min: 0
|
||||
x_max: 720
|
||||
y_min: 0
|
||||
y_max: 1280
|
||||
id: touch
|
||||
on_touch:
|
||||
- logger.log: "LVGL resuming"
|
||||
- lvgl.resume:
|
||||
- light.turn_on: backlight
|
||||
# on_release:
|
||||
# - media_player.stop:
|
||||
|
||||
esp_ldo:
|
||||
- voltage: 2.5V
|
||||
channel: 3
|
||||
|
||||
display:
|
||||
- platform: mipi_dsi
|
||||
id: lcd
|
||||
dimensions:
|
||||
height: 1280
|
||||
width: 720
|
||||
model: M5STACK-TAB5-V2
|
||||
reset_pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 4
|
||||
|
||||
output:
|
||||
- platform: ledc
|
||||
pin: GPIO22
|
||||
id: backlight_pwm
|
||||
frequency: 1000Hz
|
||||
|
||||
light:
|
||||
- platform: monochromatic
|
||||
output: backlight_pwm
|
||||
name: "Display Backlight"
|
||||
id: backlight
|
||||
restore_mode: ALWAYS_ON
|
||||
default_transition_length: 250ms
|
||||
initial_state:
|
||||
brightness: "50%"
|
||||
|
||||
image:
|
||||
defaults:
|
||||
type: rgb565
|
||||
transparency: alpha_channel
|
||||
resize: 512x512
|
||||
byte_order: little_endian
|
||||
images:
|
||||
- file: "images/va_idle.png"
|
||||
id: va_idle
|
||||
- file: "images/va_listen.png"
|
||||
id: va_listen
|
||||
- file: "images/va_speak.png"
|
||||
id: va_speak
|
||||
- file: "images/charging.png"
|
||||
id: charging_icon
|
||||
resize: 64x64
|
||||
|
||||
lvgl:
|
||||
byte_order: little_endian
|
||||
|
||||
on_idle:
|
||||
timeout: 120s
|
||||
then:
|
||||
- logger.log: "LVGL is idle"
|
||||
- light.turn_off:
|
||||
id: backlight
|
||||
transition_length: 15s
|
||||
- lvgl.pause:
|
||||
widgets:
|
||||
- obj:
|
||||
align: TOP_MID
|
||||
width: 100%
|
||||
height: 100%
|
||||
layout:
|
||||
type: flex
|
||||
flex_flow: column
|
||||
flex_align_main: START
|
||||
flex_align_track: center
|
||||
flex_align_cross: center
|
||||
widgets:
|
||||
- label:
|
||||
align: TOP_MID
|
||||
id: lbl_status
|
||||
text_font: montserrat_48
|
||||
text: "CONNECTING..."
|
||||
- label:
|
||||
align: TOP_MID
|
||||
id: lbl_ap
|
||||
text_font: montserrat_22
|
||||
text: "CONNECTING..."
|
||||
- label:
|
||||
align: TOP_MID
|
||||
id: lbl_ip
|
||||
text_font: montserrat_22
|
||||
text: "CONNECTING..."
|
||||
- image:
|
||||
id: listen_icon_widget
|
||||
src: va_idle
|
||||
align: CENTER
|
||||
- label:
|
||||
align: BOTTOM_LEFT
|
||||
id: lbl_version
|
||||
text_font: montserrat_12
|
||||
text: "v0.6"
|
||||
- label:
|
||||
align: BOTTOM_RIGHT
|
||||
id: lbl_battery
|
||||
text_font: montserrat_28
|
||||
text: Loading...
|
||||
- image:
|
||||
id: charging_icon_widget
|
||||
src: charging_icon
|
||||
align: TOP_RIGHT
|
||||
- button:
|
||||
id: volume_up_widget
|
||||
widgets:
|
||||
- label:
|
||||
text: "\uF028"
|
||||
text_font: montserrat_48
|
||||
text_align: CENTER
|
||||
align: CENTER
|
||||
x: 20
|
||||
y: 20
|
||||
width: 100
|
||||
height: 100
|
||||
pad_all: 8
|
||||
- button:
|
||||
id: volume_down_widget
|
||||
widgets:
|
||||
- label:
|
||||
text: "\uF027"
|
||||
text_font: montserrat_48
|
||||
text_align: CENTER
|
||||
align: CENTER
|
||||
x: 20
|
||||
y: 140
|
||||
width: 100
|
||||
height: 100
|
||||
pad_all: 8
|
||||
- label:
|
||||
x: 20
|
||||
y: 260
|
||||
id: lbl_volume
|
||||
text_font: montserrat_28
|
||||
text: !lambda return "%.1f",to_string(id(tab5_media_player).volume);
|
||||
|
||||
# The DAC Output select needs to be manually (or with an automation) changed to `LINE1` for the onboard speaker
|
||||
select:
|
||||
- platform: es8388
|
||||
dac_output:
|
||||
name: DAC Output
|
||||
id: dac_output
|
||||
adc_input_mic:
|
||||
name: ADC Input Mic
|
||||
id: adc_input
|
||||
|
||||
- platform: template
|
||||
id: wifi_antenna_select
|
||||
name: "WiFi Antenna"
|
||||
options:
|
||||
- "Internal"
|
||||
- "External"
|
||||
optimistic: true
|
||||
on_value:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return i == 0;
|
||||
then:
|
||||
- switch.turn_off: wifi_antenna_int_ext
|
||||
else:
|
||||
- switch.turn_on: wifi_antenna_int_ext
|
||||
|
||||
i2s_audio:
|
||||
- id: mic_bus
|
||||
i2s_lrclk_pin: GPIO29
|
||||
i2s_bclk_pin: GPIO27
|
||||
i2s_mclk_pin: GPIO30
|
||||
|
||||
audio_adc:
|
||||
- platform: es7210
|
||||
id: es7210_adc
|
||||
bits_per_sample: 16bit
|
||||
sample_rate: 16000
|
||||
|
||||
microphone:
|
||||
- platform: i2s_audio
|
||||
id: tab5_microphone
|
||||
i2s_din_pin: GPIO28
|
||||
sample_rate: 16000
|
||||
bits_per_sample: 16bit
|
||||
adc_type: external
|
||||
|
||||
audio_dac:
|
||||
- platform: es8388
|
||||
id: es8388_dac
|
||||
|
||||
speaker:
|
||||
- platform: i2s_audio
|
||||
id: tab5_speaker
|
||||
i2s_dout_pin: GPIO26
|
||||
audio_dac: es8388_dac
|
||||
dac_type: external
|
||||
channel: mono
|
||||
buffer_duration: 100ms
|
||||
bits_per_sample: 16bit
|
||||
sample_rate: 48000
|
||||
|
||||
media_player:
|
||||
- platform: speaker
|
||||
name: None
|
||||
id: tab5_media_player
|
||||
announcement_pipeline:
|
||||
speaker: tab5_speaker
|
||||
format: WAV
|
||||
|
||||
micro_wake_word:
|
||||
id: mww
|
||||
models:
|
||||
- okay_nabu
|
||||
- hey_mycroft
|
||||
- hey_jarvis
|
||||
on_wake_word_detected:
|
||||
- voice_assistant.start:
|
||||
wake_word: !lambda return wake_word;
|
||||
|
||||
voice_assistant:
|
||||
id: va
|
||||
microphone: tab5_microphone
|
||||
media_player: tab5_media_player
|
||||
micro_wake_word: mww
|
||||
on_listening:
|
||||
- logger.log: "LVGL resuming"
|
||||
- lvgl.resume:
|
||||
- light.turn_on: backlight
|
||||
- lvgl.image.update:
|
||||
id: listen_icon_widget
|
||||
src: va_listen
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "LISTENING"
|
||||
on_stt_vad_end:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "PROCESSING"
|
||||
- lvgl.image.update:
|
||||
id: listen_icon_widget
|
||||
src: va_idle
|
||||
on_tts_start:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "RESPONDING"
|
||||
- lvgl.image.update:
|
||||
id: listen_icon_widget
|
||||
src: va_speak
|
||||
on_end:
|
||||
# Wait a short amount of time to see if an announcement starts
|
||||
- wait_until:
|
||||
condition:
|
||||
- media_player.is_announcing:
|
||||
timeout: 0.5s
|
||||
# Announcement is finished and the I2S bus is free
|
||||
- wait_until:
|
||||
- and:
|
||||
- not:
|
||||
media_player.is_announcing:
|
||||
- not:
|
||||
speaker.is_playing:
|
||||
- micro_wake_word.start:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "IDLE"
|
||||
- lvgl.image.update:
|
||||
id: listen_icon_widget
|
||||
src: va_idle
|
||||
- light.turn_off:
|
||||
id: backlight
|
||||
transition_length: 15s
|
||||
on_client_connected:
|
||||
- micro_wake_word.start:
|
||||
on_client_disconnected:
|
||||
- micro_wake_word.stop:
|
||||
@@ -1,6 +1,18 @@
|
||||
esphome:
|
||||
name: tab1
|
||||
friendly_name: M5Stack Tab5 1
|
||||
name: tab2
|
||||
friendly_name: M5Stack Tab5 2
|
||||
on_boot:
|
||||
# Set the charing icon to the correct state on boot
|
||||
- then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(charging).state;
|
||||
then:
|
||||
- lvgl.widget.show:
|
||||
id: charging_icon_widget
|
||||
else:
|
||||
- lvgl.widget.hide:
|
||||
id: charging_icon_widget
|
||||
|
||||
esp32:
|
||||
board: esp32-p4-evboard
|
||||
@@ -43,6 +55,7 @@ ota:
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
fast_connect: true
|
||||
on_connect:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
@@ -54,6 +67,29 @@ wifi:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "DISCONNECTED"
|
||||
# ap:
|
||||
# password: !secret hotspot_password
|
||||
# ap_timeout: 90s
|
||||
|
||||
# captive_portal:
|
||||
|
||||
time:
|
||||
- platform: sntp
|
||||
id: sntp_time
|
||||
timezone: America/New_York
|
||||
servers:
|
||||
- 0.pool.ntp.org
|
||||
- 1.pool.ntp.org
|
||||
- 2.pool.ntp.org
|
||||
|
||||
# wireguard:
|
||||
# address: !secret tab1_wg_ip
|
||||
# private_key: !secret tab1_wg_pk
|
||||
# peer_endpoint: !secret wg_host
|
||||
# peer_public_key: !secret wg_pubkey
|
||||
|
||||
# # Optional keepalive (disabled by default)
|
||||
# peer_persistent_keepalive: 25s
|
||||
|
||||
i2c:
|
||||
- id: bsp_bus
|
||||
@@ -142,6 +178,17 @@ binary_sensor:
|
||||
pi4ioe5v6408: pi4ioe2
|
||||
number: 6
|
||||
mode: INPUT_PULLDOWN
|
||||
on_state:
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(charging).state;
|
||||
then:
|
||||
- lvgl.widget.show:
|
||||
id: charging_icon_widget
|
||||
else:
|
||||
- lvgl.widget.hide:
|
||||
id: charging_icon_widget
|
||||
|
||||
- platform: gpio
|
||||
id: headphone_detect
|
||||
@@ -186,7 +233,7 @@ sensor:
|
||||
- lvgl.label.update:
|
||||
id: lbl_battery
|
||||
text:
|
||||
format: "Battery: %.1f%"
|
||||
format: "Battery: %.1f%%"
|
||||
args: ["id(battery_percent).state"]
|
||||
|
||||
touchscreen:
|
||||
@@ -255,6 +302,9 @@ image:
|
||||
id: va_listen
|
||||
- file: "images/va_speak.png"
|
||||
id: va_speak
|
||||
- file: "images/charging.png"
|
||||
id: charging_icon
|
||||
resize: 64x64
|
||||
|
||||
lvgl:
|
||||
byte_order: little_endian
|
||||
@@ -287,6 +337,25 @@ lvgl:
|
||||
id: lbl_battery
|
||||
text_font: montserrat_28
|
||||
text: Loading...
|
||||
- image:
|
||||
id: charging_icon_widget
|
||||
src: charging_icon
|
||||
align: TOP_RIGHT
|
||||
- slider:
|
||||
id: backlight_slider
|
||||
x: 20
|
||||
y: 50
|
||||
width: 30
|
||||
height: 220
|
||||
pad_all: 8
|
||||
min_value: 0
|
||||
max_value: 255
|
||||
on_release:
|
||||
- homeassistant.action:
|
||||
action: light.turn_on
|
||||
data:
|
||||
entity_id: light.backlight
|
||||
brightness: !lambda return int(x);
|
||||
|
||||
# The DAC Output select needs to be manually (or with an automation) changed to `LINE1` for the onboard speaker
|
||||
select:
|
||||
13
active/device_unifi/config.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
rabbitmq:
|
||||
host: "rabbitmq.reeselink.com"
|
||||
port: 5672
|
||||
virtual_host: "/"
|
||||
username: "user"
|
||||
password: "password"
|
||||
|
||||
# Which *exchange* (topic) you actually want to listen to.
|
||||
# The program will create a temporary queue, bind it to this exchange
|
||||
# with the routing key supplied in `routing_key`.
|
||||
subscriber:
|
||||
exchange: "nic" # ← change to “reese” or any other exchange
|
||||
routing_key: "add" # ← could be “add”, “delete”, or any pattern
|
||||
136
active/device_unifi/rmq_dns.py
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RabbitMQ setup & consumer using pika.
|
||||
|
||||
- Creates two **topic exchanges**: nic, reese
|
||||
- For each exchange creates two queues: add, delete
|
||||
- Binds the queues with routing keys “add” and “delete”
|
||||
- Subscribes (consumes) from a **single** exchange/queue pair that is
|
||||
supplied via a tiny config file (config.yaml).
|
||||
|
||||
Run:
|
||||
python3 rabbit_demo.py
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pika # type: ignore
|
||||
import yaml
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 1️⃣ Load configuration
|
||||
# ----------------------------------------------------------------------
|
||||
DEFAULT_CFG = """
|
||||
rabbitmq:
|
||||
host: "localhost"
|
||||
port: 5672
|
||||
virtual_host: "/"
|
||||
username: "guest"
|
||||
password: "guest"
|
||||
|
||||
# Which *exchange* (topic) you actually want to listen to.
|
||||
# The program will create a temporary queue, bind it to this exchange
|
||||
# with the routing key supplied in `routing_key`.
|
||||
subscriber:
|
||||
exchange: "nic" # ← change to “reese” or any other exchange
|
||||
routing_key: "add" # ← could be “add”, “delete”, or any pattern
|
||||
"""
|
||||
|
||||
CONFIG_PATH = Path("active/device_unifi/config.yaml")
|
||||
if not CONFIG_PATH.exists():
|
||||
CONFIG_PATH.write_text(DEFAULT_CFG)
|
||||
|
||||
with CONFIG_PATH.open() as f:
|
||||
cfg = yaml.safe_load(f)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 2️⃣ Build connection parameters
|
||||
# ----------------------------------------------------------------------
|
||||
cred = pika.PlainCredentials(cfg["rabbitmq"]["username"], cfg["rabbitmq"]["password"])
|
||||
params = pika.ConnectionParameters(
|
||||
host=cfg["rabbitmq"]["host"],
|
||||
port=cfg["rabbitmq"]["port"],
|
||||
virtual_host=cfg["rabbitmq"]["virtual_host"],
|
||||
credentials=cred,
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 3️⃣ Helper to declare exchanges / queues
|
||||
# ----------------------------------------------------------------------
|
||||
def declare_topology(channel):
|
||||
"""
|
||||
Create the two topic exchanges and the four queues,
|
||||
then bind each queue to its exchange with the appropriate routing key.
|
||||
"""
|
||||
exchanges = ["nic", "reese"]
|
||||
routing_keys = ["add", "delete"]
|
||||
|
||||
for exch in exchanges:
|
||||
channel.exchange_declare(exchange=exch, exchange_type="topic", durable=True)
|
||||
for key in routing_keys:
|
||||
queue_name = f"{exch}_{key}" # e.g. nic_add, reese_delete
|
||||
channel.queue_declare(queue=queue_name, durable=True)
|
||||
# bind queue to the exchange with the same routing key
|
||||
channel.queue_bind(queue=queue_name, exchange=exch, routing_key=key)
|
||||
logging.info(
|
||||
f"Declared queue {queue_name} bound to {exch} with key '{key}'"
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 4️⃣ Consumer callback
|
||||
# ----------------------------------------------------------------------
|
||||
def on_message(ch, method, properties, body):
|
||||
logging.info(
|
||||
f"Received from exchange '{method.exchange}' "
|
||||
f"routing_key='{method.routing_key}': {body!r}"
|
||||
)
|
||||
# Acknowledge the message
|
||||
ch.basic_ack(delivery_tag=method.delivery_tag)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 5️⃣ Main routine
|
||||
# ----------------------------------------------------------------------
|
||||
def main():
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s"
|
||||
)
|
||||
|
||||
with pika.BlockingConnection(params) as conn:
|
||||
channel = conn.channel()
|
||||
|
||||
# 1️⃣ Declare the static topology (exchanges + queues)
|
||||
declare_topology(channel)
|
||||
|
||||
# 2️⃣ Set up a *temporary* queue for the subscriber defined in config
|
||||
result = channel.queue_declare(
|
||||
queue="", exclusive=True
|
||||
) # server‑generated name
|
||||
tmp_queue = result.method.queue
|
||||
|
||||
exch = cfg["subscriber"]["exchange"]
|
||||
rkey = cfg["subscriber"]["routing_key"]
|
||||
channel.queue_bind(queue=tmp_queue, exchange=exch, routing_key=rkey)
|
||||
|
||||
logging.info(
|
||||
f"Subscribed to exchange '{exch}' with routing_key '{rkey}' "
|
||||
f"using temporary queue '{tmp_queue}'"
|
||||
)
|
||||
|
||||
# 3️⃣ Start consuming
|
||||
channel.basic_consume(queue=tmp_queue, on_message_callback=on_message)
|
||||
|
||||
try:
|
||||
logging.info("Waiting for messages. Press Ctrl+C to exit.")
|
||||
channel.start_consuming()
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Interrupted – closing connection.")
|
||||
channel.stop_consuming()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
46
active/device_unifi/rmq_dns_test.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pika
|
||||
import yaml
|
||||
|
||||
DEFAULT_CFG = """
|
||||
rabbitmq:
|
||||
host: "localhost"
|
||||
port: 5672
|
||||
virtual_host: "/"
|
||||
username: "guest"
|
||||
password: "guest"
|
||||
|
||||
# Which *exchange* (topic) you actually want to listen to.
|
||||
# The program will create a temporary queue, bind it to this exchange
|
||||
# with the routing key supplied in `routing_key`.
|
||||
subscriber:
|
||||
exchange: "nic" # ← change to “reese” or any other exchange
|
||||
routing_key: "add" # ← could be “add”, “delete”, or any pattern
|
||||
"""
|
||||
|
||||
CONFIG_PATH = Path("active/device_unifi/config.yaml")
|
||||
if not CONFIG_PATH.exists():
|
||||
CONFIG_PATH.write_text(DEFAULT_CFG)
|
||||
|
||||
with CONFIG_PATH.open() as f:
|
||||
cfg = yaml.safe_load(f)
|
||||
|
||||
|
||||
cred = pika.PlainCredentials(cfg["rabbitmq"]["username"], cfg["rabbitmq"]["password"])
|
||||
params = pika.ConnectionParameters(
|
||||
host=cfg["rabbitmq"]["host"],
|
||||
port=cfg["rabbitmq"]["port"],
|
||||
virtual_host=cfg["rabbitmq"]["virtual_host"],
|
||||
credentials=cred,
|
||||
)
|
||||
|
||||
with pika.BlockingConnection(params) as c:
|
||||
ch = c.channel()
|
||||
ch.basic_publish(
|
||||
exchange="reese",
|
||||
routing_key="add",
|
||||
body=json.dumps({"msg": "hello nic add"}),
|
||||
properties=pika.BasicProperties(delivery_mode=2), # make it persistent
|
||||
)
|
||||
15
active/device_unifi/test_update_dns.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from update_dns import ApiHelperMethods, ApiPaths, ApiWrapper
|
||||
|
||||
|
||||
def test_api_get():
|
||||
assert ApiWrapper.api_get(ApiPaths.list_sites()) is not None
|
||||
|
||||
|
||||
def test_site_name_to_id():
|
||||
assert ApiHelperMethods.site_name_to_id("Default") is not None
|
||||
|
||||
|
||||
def test_dns_record_exists():
|
||||
assert ApiHelperMethods.dns_record_exists("test.reeselink.com")
|
||||
|
||||
assert not ApiHelperMethods.dns_record_exists("idontexist.reeselink.com")
|
||||
58
active/device_unifi/unifi.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Unifi
|
||||
|
||||
## Update DNS Records via API
|
||||
|
||||
```bash
|
||||
export API_KEY=$(cat active/device_unifi/secrets/api-key)
|
||||
|
||||
# List site IDs
|
||||
curl -L -g -k -s "https://192.168.1.1/proxy/network/integration/v1/sites" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" | jq -rc '.data[0].id'
|
||||
|
||||
# List domains
|
||||
curl -L -g -k -s "https://192.168.1.1/proxy/network/integration/v1/sites/88f7af54-98f8-306a-a1c7-c9349722b1f6/dns/policies" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" | jq -r '.data[] | {domain, id}'
|
||||
|
||||
# List device domains
|
||||
curl -L -g -k -s "https://10.1.0.1/proxy/network/v2/api/site/default/static-dns/devices" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" | jq -r
|
||||
|
||||
# List clients
|
||||
curl -L -g -k -s "https://192.168.1.1/proxy/network/integration/v1/sites/88f7af54-98f8-306a-a1c7-c9349722b1f6/clients" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}"
|
||||
|
||||
# List firewall policies
|
||||
curl -L -g -k -s "https://192.168.1.1/proxy/network/integration/v1/sites/88f7af54-98f8-306a-a1c7-c9349722b1f6/firewall/policies" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}"
|
||||
|
||||
# Create a record
|
||||
curl -L -g -k -s "https://192.168.1.1/proxy/network/integration/v1/sites/88f7af54-98f8-306a-a1c7-c9349722b1f6/dns/policies" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"type\": \"A_RECORD\",
|
||||
\"enabled\": true,
|
||||
\"domain\": \"test.reeselink.com\",
|
||||
\"ipv4Address\": \"10.1.0.100\",
|
||||
\"ttlSeconds\": 300
|
||||
}"
|
||||
|
||||
# Update a record
|
||||
curl -L -g -k -s -X PUT "https://192.168.1.1/proxy/network/integration/v1/sites/88f7af54-98f8-306a-a1c7-c9349722b1f6/dns/policies/a5689d61-811a-48b0-a47c-2ece038e4356" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"type\": \"A_RECORD\",
|
||||
\"enabled\": true,
|
||||
\"domain\": \"test.reeselink.com\",
|
||||
\"ipv4Address\": \"10.1.0.100\",
|
||||
\"ttlSeconds\": 300
|
||||
}"
|
||||
```
|
||||
103
active/device_unifi/update_dns.py
Normal file
@@ -0,0 +1,103 @@
|
||||
import json
|
||||
import os
|
||||
from typing import TypedDict
|
||||
|
||||
import requests
|
||||
|
||||
UNIFI_API_ENDPOINT = os.getenv("UNIFI_API_ENDPOINT", "https://192.168.1.1")
|
||||
UNIFI_SITE_NAME = os.getenv("UNIFI_SITE_NAME", "Default")
|
||||
UNIFI_API_KEY = os.getenv("UNIFI_API_KEY")
|
||||
|
||||
if not UNIFI_API_KEY:
|
||||
try:
|
||||
with open("active/device_unifi/secrets/api-key", "r") as f:
|
||||
UNIFI_API_KEY = f.read()
|
||||
except (FileNotFoundError, PermissionError) as e:
|
||||
print(e)
|
||||
print("UNIFI_API_KEY required.")
|
||||
|
||||
type uuid_type = str
|
||||
|
||||
|
||||
class UnifiSite(TypedDict):
|
||||
id: uuid_type
|
||||
internalReference: str
|
||||
name: str
|
||||
|
||||
|
||||
class ApiPaths:
|
||||
@classmethod
|
||||
def list_sites(cls) -> str:
|
||||
return "/proxy/network/integration/v1/sites"
|
||||
|
||||
@classmethod
|
||||
def list_records(cls, site_id: uuid_type) -> str:
|
||||
return f"/proxy/network/integration/v1/sites/{site_id}/dns/policies"
|
||||
|
||||
@classmethod
|
||||
def create_record(cls, site_id: uuid_type) -> str:
|
||||
return f"/proxy/network/integration/v1/sites/{site_id}/dns/policies"
|
||||
|
||||
@classmethod
|
||||
def update_record(cls, site_id: uuid_type, record_id: uuid_type) -> str:
|
||||
return f"/proxy/network/integration/v1/sites/{site_id}/dns/policies/{record_id}"
|
||||
|
||||
|
||||
class ApiWrapper:
|
||||
@classmethod
|
||||
def api_get(cls, path: str):
|
||||
return requests.get(
|
||||
f"{UNIFI_API_ENDPOINT}{path}",
|
||||
headers={"X-API-Key": UNIFI_API_KEY},
|
||||
verify=False,
|
||||
).json()
|
||||
|
||||
@classmethod
|
||||
def api_put(cls, path: str, body: dict):
|
||||
return requests.put(
|
||||
f"{UNIFI_API_ENDPOINT}{path}",
|
||||
headers={"X-API-Key": UNIFI_API_KEY},
|
||||
verify=False,
|
||||
json=json.dumps(body),
|
||||
).json()
|
||||
|
||||
|
||||
class ApiHelperMethods:
|
||||
@classmethod
|
||||
def site_name_to_id(cls, site_name: str) -> uuid_type:
|
||||
results = ApiWrapper.api_get(ApiPaths.list_sites())
|
||||
data: list[UnifiSite] = results.get("data")
|
||||
if not data:
|
||||
print("No sites found")
|
||||
exit(1)
|
||||
filtered_sites: list[UnifiSite] = list(
|
||||
filter(lambda data_item: data_item["name"] == site_name, data)
|
||||
)
|
||||
if not filtered_sites:
|
||||
print("Site with that name not found")
|
||||
exit(1)
|
||||
site_id = filtered_sites[0]["id"]
|
||||
return site_id
|
||||
|
||||
# @classmethod
|
||||
# def upsert_dns_ipv4_record(cls, record_name: str, ipv4_addr: str) -> uuid_type:
|
||||
|
||||
@classmethod
|
||||
def dns_record_exists(cls, record_name: str) -> bool:
|
||||
site_id = ApiHelperMethods.site_name_to_id(UNIFI_SITE_NAME)
|
||||
record_id = ApiWrapper.api_get(path=ApiPaths.list_records(site_id))
|
||||
matched_dns_records = list(
|
||||
filter(
|
||||
lambda record_item: record_item["domain"] == record_name,
|
||||
record_id["data"],
|
||||
)
|
||||
)
|
||||
if matched_dns_records:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
site_name = sys.argv[1]
|
||||
90
active/kubernetes/.$Cluster2026.drawio.bkp
Normal file
@@ -0,0 +1,90 @@
|
||||
<mxfile host="Electron" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/29.6.6 Chrome/144.0.7559.236 Electron/40.8.4 Safari/537.36" version="29.6.6">
|
||||
<diagram name="Page-1" id="sur_P5ccan6r_R6vxB1T">
|
||||
<mxGraphModel dx="1243" dy="832" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-23" parent="1" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=none;" value="Client" vertex="1">
|
||||
<mxGeometry height="440" width="200" x="840" y="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-22" parent="1" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=none;" value="Network Admin" vertex="1">
|
||||
<mxGeometry height="440" width="200" x="560" y="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-21" parent="1" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=none;" value="Developer" vertex="1">
|
||||
<mxGeometry height="440" width="200" y="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-20" parent="1" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=none;" value="Cluster Admin" vertex="1">
|
||||
<mxGeometry height="440" width="200" x="280" y="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-4" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-3" value="Create Record">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-1" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="External DNS" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="160" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-13" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-8" value="Connect">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-19" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-3" value="Get IP">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="940" y="190" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-2" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Browser" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="880" y="240" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-3" parent="1" style="ellipse;shape=cloud;whiteSpace=wrap;html=1;" value="Route53" vertex="1">
|
||||
<mxGeometry height="80" width="120" x="600" y="150" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-9" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-8" value="Allocate IP">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-7" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Metal LB" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="240" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-12" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-11" value="Forward to Gateway">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="660" y="350" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-8" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Router/Proxy" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="600" y="240" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-18" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-10" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-15" value="Create Storage">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-10" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Longhorn" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="480" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-17" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-14" value="Forward to Container">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-11" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Traefik" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-16" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-15">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-14" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Container" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="40" y="320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-15" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Storage" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="40" y="400" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-26" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-24" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-25" value="Request Certificate">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-24" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Cert Manager" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="400" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-25" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="CA<div>(Let's Encrypt)</div>" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="600" y="400" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
96
active/kubernetes/Cluster2026.drawio
Normal file
@@ -0,0 +1,96 @@
|
||||
<mxfile host="Electron" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/29.6.6 Chrome/144.0.7559.236 Electron/40.8.4 Safari/537.36" version="29.6.6">
|
||||
<diagram name="Page-1" id="sur_P5ccan6r_R6vxB1T">
|
||||
<mxGraphModel dx="1243" dy="832" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-23" parent="1" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=none;" value="Client" vertex="1">
|
||||
<mxGeometry height="520" width="200" x="840" y="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-22" parent="1" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=none;" value="Network Admin" vertex="1">
|
||||
<mxGeometry height="520" width="200" x="560" y="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-21" parent="1" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=none;" value="Developer" vertex="1">
|
||||
<mxGeometry height="520" width="200" y="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-20" parent="1" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=none;" value="Cluster Admin" vertex="1">
|
||||
<mxGeometry height="520" width="200" x="280" y="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-4" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-3" value="Create Record">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-1" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="External DNS" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="160" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-13" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-8" value="Connect">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-19" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-3" value="Get IP">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="940" y="190" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-2" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Browser" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="880" y="240" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-3" parent="1" style="ellipse;shape=cloud;whiteSpace=wrap;html=1;" value="Route53" vertex="1">
|
||||
<mxGeometry height="80" width="120" x="600" y="150" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-9" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-8" value="Allocate IP">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-7" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Metal LB" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="240" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-12" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-11" value="Forward to Gateway">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="660" y="350" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-8" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Router/Proxy" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="600" y="240" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-18" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-10" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-15" value="Create Storage">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-10" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Longhorn" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="480" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-17" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-14" value="Forward to Container">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-11" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Traefik" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-16" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-15">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-14" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Container" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="40" y="320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-15" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Storage" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="40" y="400" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-26" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-24" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-25" value="Request Certificate">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-24" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Cert Manager" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="400" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-25" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="CA<div>(Let's Encrypt)</div>" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="600" y="400" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-29" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-28" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-14" value="Create Internal Record">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-28" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="CoreDNS" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="80" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
34
active/kubernetes_external-dns/demo-app.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx
|
||||
annotations:
|
||||
external-dns.alpha.kubernetes.io/hostname: external-dns.reeselink.com
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- port: 80
|
||||
name: http
|
||||
targetPort: 80
|
||||
selector:
|
||||
app: nginx
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx
|
||||
name: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
@@ -1,80 +0,0 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
labels:
|
||||
app.kubernetes.io/name: external-dns
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services","endpoints","pods","nodes"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: ["extensions","networking.k8s.io"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
labels:
|
||||
app.kubernetes.io/name: external-dns
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: external-dns
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: external-dns
|
||||
namespace: kube-system # change to desired namespace: externaldns, kube-addons
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
namespace: kube-system
|
||||
labels:
|
||||
app.kubernetes.io/name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: external-dns
|
||||
spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.k8s.io/external-dns/external-dns:v0.14.2
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
- --domain-filter=reeseapps.com
|
||||
- --provider=aws
|
||||
- --aws-zone-type=public
|
||||
- --registry=txt
|
||||
# - --txt-owner-id=external-dns
|
||||
env:
|
||||
- name: AWS_DEFAULT_REGION
|
||||
value: us-east-1 # change to region where EKS is installed
|
||||
- name: AWS_SHARED_CREDENTIALS_FILE
|
||||
value: /.aws/externaldns-credentials
|
||||
volumeMounts:
|
||||
- name: aws-credentials
|
||||
mountPath: /.aws
|
||||
readOnly: true
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "500m"
|
||||
|
||||
volumes:
|
||||
- name: aws-credentials
|
||||
secret:
|
||||
secretName: external-dns
|
||||
@@ -1,8 +0,0 @@
|
||||
# comment out sa if it was previously created
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
namespace: kube-system
|
||||
labels:
|
||||
app.kubernetes.io/name: external-dns
|
||||
31
active/kubernetes_longhorn/demo-app.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: storage-test-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
# If you want to test a specific storage class, uncomment the line below:
|
||||
# storageClassName: <your-storage-class-name>
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: storage-test-pod
|
||||
spec:
|
||||
containers:
|
||||
- name: busybox
|
||||
image: busybox
|
||||
command:
|
||||
["sh", "-c", "while true; do date >> /mnt/test.txt; sleep 10; done"]
|
||||
volumeMounts:
|
||||
- name: test-volume
|
||||
mountPath: /mnt
|
||||
restartPolicy: Always
|
||||
volumes:
|
||||
- name: test-volume
|
||||
persistentVolumeClaim:
|
||||
claimName: storage-test-pvc
|
||||
@@ -6,7 +6,7 @@ metadata:
|
||||
namespace: metallb-system
|
||||
spec:
|
||||
addresses:
|
||||
- 10.4.1.1-10.4.3.254
|
||||
- 10.4.2.32-10.4.2.47
|
||||
|
||||
---
|
||||
apiVersion: metallb.io/v1beta1
|
||||
|
||||
@@ -9,4 +9,4 @@ spec:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 32Gi
|
||||
storage: 8Gi
|
||||
|
||||
@@ -2,12 +2,17 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Release.Name }}
|
||||
annotations:
|
||||
metallb.universe.tf/address-pool: "default-pool"
|
||||
spec:
|
||||
externalTrafficPolicy: Cluster
|
||||
selector:
|
||||
app: {{ .Release.Name }}
|
||||
ports:
|
||||
- port: {{ .Values.port }}
|
||||
targetPort: 25565
|
||||
name: {{ .Release.Name }}
|
||||
ipFamilyPolicy: PreferDualStack
|
||||
ipFamilies:
|
||||
- IPv4
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- name: minecraft
|
||||
protocol: TCP
|
||||
port: {{ .Values.port }}
|
||||
targetPort: 25565
|
||||
selector:
|
||||
app.kubernetes.io/name: {{ .Release.Name }}
|
||||
|
||||
@@ -4,7 +4,7 @@ get_server:
|
||||
server_version: "1.21.3"
|
||||
port: 25565
|
||||
max_cpu: 4
|
||||
max_ram: 8
|
||||
max_ram: 2
|
||||
server_props: |
|
||||
enable-jmx-monitoring=false
|
||||
rcon.port=25575
|
||||
|
||||
54
active/kubernetes_traefik/demo-app.yaml
Normal file
@@ -0,0 +1,54 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: whoami
|
||||
namespace: default
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: whoami
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: whoami
|
||||
spec:
|
||||
containers:
|
||||
- name: whoami
|
||||
image: traefik/whoami
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: whoami
|
||||
namespace: default
|
||||
spec:
|
||||
selector:
|
||||
app: whoami
|
||||
ports:
|
||||
- port: 80
|
||||
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: whoami
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: traefik
|
||||
hostnames:
|
||||
- "traefik-reese.reeselink.com"
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
||||
backendRefs:
|
||||
- name: whoami
|
||||
namespace: default
|
||||
port: 80
|
||||
96
active/kubernetes_traefik/values.yaml
Normal file
@@ -0,0 +1,96 @@
|
||||
# Configure Network Ports and EntryPoints
|
||||
# EntryPoints are the network listeners for incoming traffic.
|
||||
ports:
|
||||
# Defines the HTTP entry point named 'web'
|
||||
web:
|
||||
port: 80
|
||||
nodePort: 30000
|
||||
# Instructs this entry point to redirect all traffic to the 'websecure' entry point
|
||||
http:
|
||||
redirections:
|
||||
entryPoint:
|
||||
to: websecure
|
||||
scheme: https
|
||||
permanent: true
|
||||
|
||||
# Defines the HTTPS entry point named 'websecure'
|
||||
websecure:
|
||||
port: 443
|
||||
nodePort: 30001
|
||||
|
||||
# Enables the dashboard in Secure Mode
|
||||
api:
|
||||
dashboard: true
|
||||
insecure: false
|
||||
|
||||
ingressRoute:
|
||||
dashboard:
|
||||
enabled: true
|
||||
matchRule: Host(`traefik-dashboard.reeselink.com`)
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- name: dashboard-auth
|
||||
|
||||
# Creates a BasicAuth Middleware and Secret for the Dashboard Security
|
||||
extraObjects:
|
||||
- apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: dashboard-auth-secret
|
||||
type: kubernetes.io/basic-auth
|
||||
stringData:
|
||||
username: admin
|
||||
password: "P@ssw0rd" # Replace with an Actual Password
|
||||
- apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: dashboard-auth
|
||||
spec:
|
||||
basicAuth:
|
||||
secret: dashboard-auth-secret
|
||||
|
||||
# We will route with Gateway API instead.
|
||||
ingressClass:
|
||||
enabled: false
|
||||
|
||||
# Enable Gateway API Provider & Disables the KubernetesIngress provider
|
||||
# Providers tell Traefik where to find routing configuration.
|
||||
providers:
|
||||
kubernetesIngress:
|
||||
enabled: false
|
||||
kubernetesGateway:
|
||||
enabled: true
|
||||
|
||||
## Gateway Listeners
|
||||
gateway:
|
||||
listeners:
|
||||
web: # HTTP listener that matches entryPoint `web`
|
||||
port: 80
|
||||
protocol: HTTP
|
||||
namespacePolicy:
|
||||
from: All
|
||||
|
||||
websecure: # HTTPS listener that matches entryPoint `websecure`
|
||||
port: 443
|
||||
protocol: HTTPS # TLS terminates inside Traefik
|
||||
namespacePolicy:
|
||||
from: All
|
||||
mode: Terminate
|
||||
certificateRefs:
|
||||
- kind: Secret
|
||||
name: local-selfsigned-tls # the Secret we created before the installation
|
||||
group: ""
|
||||
|
||||
# Enable Observability
|
||||
logs:
|
||||
general:
|
||||
level: INFO
|
||||
# This enables access logs, outputting them to Traefik's standard output by default. The [Access Logs Documentation](https://doc.traefik.io/traefik/observability/access-logs/) covers formatting, filtering, and output options.
|
||||
access:
|
||||
enabled: true
|
||||
|
||||
# Enables Prometheus for Metrics
|
||||
metrics:
|
||||
prometheus:
|
||||
enabled: true
|
||||
@@ -363,7 +363,8 @@ for folder in $(ls); do du --exclude .snapshots -sh $folder; done
|
||||
alias {dudir,dud}='du -h --max-depth 1 | sort -h'
|
||||
|
||||
# Calculate all file sizes in current dir
|
||||
alias {dufile,duf}='ls -lhSr'
|
||||
alias {dufile,duf}='find . -name ".snapshots" -prune -o -type f -exec du -h {} + | sort -hr'
|
||||
alias {dufiler,dufr}='find . -name ".snapshots" -prune -o -type f -exec du -h {} + | sort -h'
|
||||
```
|
||||
|
||||
### Disk Wear
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
[Network]
|
||||
IPv6=true
|
||||
Internal=true
|
||||
@@ -6,6 +6,8 @@ PublishPort=8000:8000/tcp
|
||||
PublishPort=8001:8001/tcp
|
||||
# llama.cpp instruct
|
||||
PublishPort=8002:8002/tcp
|
||||
# llama.cpp code
|
||||
PublishPort=8003:8003/tcp
|
||||
# stable-diffusion.cpp gen
|
||||
PublishPort=1234:1234/tcp
|
||||
# stable-diffusion.cpp edit
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
- [open-webui](#open-webui)
|
||||
- [lite-llm](#lite-llm)
|
||||
- [Install Services with Quadlets](#install-services-with-quadlets)
|
||||
- [API Keys](#api-keys)
|
||||
- [Internal and External Pods](#internal-and-external-pods)
|
||||
- [Llama CPP Server (Port 8000)](#llama-cpp-server-port-8000)
|
||||
- [Llama CPP Embedding Server (Port 8001)](#llama-cpp-embedding-server-port-8001)
|
||||
@@ -179,7 +180,11 @@ rsync -av --progress /home/ai/models/ /srv/models/
|
||||
|
||||
### Download models
|
||||
|
||||
In general I try to run 8 bit quantized minimum.
|
||||
In my completely subjective opinion: 5 bit quant is usually the sweet spot for
|
||||
unsloth models. Q5_K_S is usually just fine.
|
||||
|
||||
I usually download the F16 mmproj files. This is also completely subjective.
|
||||
BF16 is fine. F32 is overkill.
|
||||
|
||||
#### Text models
|
||||
|
||||
@@ -218,8 +223,13 @@ hf download --local-dir . ggml-org/Ministral-3-3B-Instruct-2512-GGUF
|
||||
##### Qwen
|
||||
|
||||
```bash
|
||||
# qwen3.6-35b-a3b
|
||||
mkdir qwen3.6-35b-a3b && cd qwen3.6-35b-a3b
|
||||
hf download --local-dir . unsloth/Qwen3.6-35B-A3B-GGUF Qwen3.6-35B-A3B-UD-Q5_K_M.gguf
|
||||
hf download --local-dir . unsloth/Qwen3.6-35B-A3B-GGUF mmproj-F16.gguf
|
||||
|
||||
# qwen3.5-27b-opus
|
||||
mkdir qwen3.5-27b-opus && qwen3.5-27b-opus
|
||||
mkdir qwen3.5-27b-opus && cd qwen3.5-27b-opus
|
||||
hf download --local-dir . Jackrong/Qwen3.5-27B-Claude-4.6-Opus-Reasoning-Distilled-GGUF Qwen3.5-27B.Q4_K_M.gguf
|
||||
hf download --local-dir . Jackrong/Qwen3.5-27B-Claude-4.6-Opus-Reasoning-Distilled-GGUF mmproj-BF16.gguf
|
||||
|
||||
@@ -555,6 +565,22 @@ podman run \
|
||||
|
||||
## Install Services with Quadlets
|
||||
|
||||
### API Keys
|
||||
|
||||
```bash
|
||||
mkdir -p /home/ai/.llama-api
|
||||
touch /home/ai/.llama-api/keys.env
|
||||
chmod 600 /home/ai/.llama-api/keys.env
|
||||
vim /home/ai/.llama-api/keys.env
|
||||
|
||||
LLAMA_API_KEY=
|
||||
|
||||
# Generate keys and append to file, then comma separate the keys
|
||||
openssl rand -base64 48 >> keys.env
|
||||
openssl rand -base64 48 >> keys.env
|
||||
openssl rand -base64 48 >> keys.env
|
||||
```
|
||||
|
||||
### Internal and External Pods
|
||||
|
||||
These will be used to restrict internet access to our llama.cpp and
|
||||
@@ -562,10 +588,10 @@ stable-diffusion.cpp services while allowing the frontend services to
|
||||
communicate with those containers.
|
||||
|
||||
```bash
|
||||
scp -r active/software_ai_stack/quadlets_pods/* deskwork-ai:.config/containers/systemd/
|
||||
scp -r active/software_ai_stack/ai-internal.* deskwork-ai:.config/containers/systemd/
|
||||
ssh deskwork-ai
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user start ai-internal-pod.service ai-external-pod.service
|
||||
systemctl --user start ai-internal-pod.service
|
||||
```
|
||||
|
||||
### Llama CPP Server (Port 8000)
|
||||
@@ -573,7 +599,7 @@ systemctl --user start ai-internal-pod.service ai-external-pod.service
|
||||
Installs the llama.cpp server to run our text models.
|
||||
|
||||
```bash
|
||||
scp -r active/software_ai_stack/quadlets_llama_think/* deskwork-ai:.config/containers/systemd/
|
||||
scp -r active/software_ai_stack/llama-think.container deskwork-ai:.config/containers/systemd/
|
||||
ssh deskwork-ai
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user restart ai-internal-pod.service
|
||||
@@ -584,7 +610,7 @@ systemctl --user restart ai-internal-pod.service
|
||||
Installs the llama.cpp server to run our embedding models
|
||||
|
||||
```bash
|
||||
scp -r active/software_ai_stack/quadlets_llama_embed/* deskwork-ai:.config/containers/systemd/
|
||||
scp -r active/software_ai_stack/llama-embed.container deskwork-ai:.config/containers/systemd/
|
||||
ssh deskwork-ai
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user restart ai-internal-pod.service
|
||||
@@ -595,7 +621,7 @@ systemctl --user restart ai-internal-pod.service
|
||||
Installs the llama.cpp server to run a constant instruct (no thinking) model for quick replies
|
||||
|
||||
```bash
|
||||
scp -r active/software_ai_stack/quadlets_llama_instruct/* deskwork-ai:.config/containers/systemd/
|
||||
scp -r active/software_ai_stack/llama-instruct.container deskwork-ai:.config/containers/systemd/
|
||||
ssh deskwork-ai
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user restart ai-internal-pod.service
|
||||
@@ -711,11 +737,11 @@ Apple M4 max
|
||||
export TOKEN=$(cat active/software_ai_stack/secrets/aipi-token)
|
||||
|
||||
# List Models
|
||||
curl https://aipi.reeseapps.com/v1/models \
|
||||
-H "Authorization: Bearer $TOKEN" | jq
|
||||
curl https://llama-instruct.reeseapps.com/v1/models \
|
||||
-H "Authorization: Bearer $TOKEN" | jq '.data'
|
||||
|
||||
# Text
|
||||
curl https://aipi.reeseapps.com/v1/chat/completions \
|
||||
curl https://llama-instruct.reeseapps.com/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
@@ -724,26 +750,21 @@ curl https://aipi.reeseapps.com/v1/chat/completions \
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Hello, how are you?"}
|
||||
],
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 500
|
||||
}' | jq
|
||||
|
||||
# Completion
|
||||
curl https://aipi.reeseapps.com/v1/completions \
|
||||
curl https://llama-instruct.reeseapps.com/v1/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"model": "llama-instruct/instruct",
|
||||
"prompt": "Write a short poem about the ocean.",
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 500,
|
||||
"top_p": 1,
|
||||
"frequency_penalty": 0,
|
||||
"presence_penalty": 0
|
||||
"max_tokens": 500
|
||||
}' | jq
|
||||
|
||||
# Image Gen
|
||||
curl https://aipi.reeseapps.com/v1/images/generations \
|
||||
curl https://image-gen.reeselink.com/v1/images/generations \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
@@ -766,11 +787,11 @@ curl http://aipi.reeseapps.com/v1/images/edits \
|
||||
|
||||
# Embed
|
||||
curl \
|
||||
"https://aipi.reeseapps.com/v1/embeddings" \
|
||||
"https://llama-embed.reeseapps.com/v1/embeddings" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "llama-embed/embed",
|
||||
"model": "deskwork-embed/embed",
|
||||
"input":"This is the reason you ended up here:",
|
||||
"encoding_format": "float"
|
||||
}'
|
||||
@@ -789,16 +810,20 @@ podman run --rm \
|
||||
--env "HF_TOKEN=$HF_TOKEN" \
|
||||
-p 8010:8000 \
|
||||
--ipc=host \
|
||||
-e ROCBLAS_USE_HIPBLASLT=1 \
|
||||
-e TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1 \
|
||||
-e VLLM_TARGET_DEVICE=rocm \
|
||||
-e HIP_FORCE_DEV_KERNARG=1 \
|
||||
-e RAY_EXPERIMENTAL_NOSET_ROCR_VISIBLE_DEVICES=1 \
|
||||
docker.io/vllm/vllm-openai-rocm:nightly \
|
||||
--enable-offline-docs \
|
||||
|
||||
# Pick your model
|
||||
Qwen/Qwen3.5-35B-A3B --max-model-len 262144 --reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser qwen3_coder
|
||||
Qwen/Qwen3.5-35B-A3B-FP8 --max-model-len 262144 --reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser qwen3_coder
|
||||
Qwen/Qwen3.5-9B --max-model-len 262144 --reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser qwen3_coder
|
||||
Qwen/Qwen3.5-35B-A3B-FP8
|
||||
google/gemma-4-26B-A4B-it
|
||||
openai/gpt-oss-120b
|
||||
|
||||
```
|
||||
|
||||
## Misc
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
- name: Create Deskwork AI Stack
|
||||
hosts: toybox-ai
|
||||
hosts: deskwork-ai
|
||||
tasks:
|
||||
- name: Create /home/ai/.config/containers/systemd
|
||||
ansible.builtin.file:
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
- ai-internal.pod
|
||||
- llama-embed.container
|
||||
- llama-instruct.container
|
||||
- llama-think.container
|
||||
- llama-code.container
|
||||
- name: Reload and start the ai-internal-pod service
|
||||
ansible.builtin.systemd_service:
|
||||
state: restarted
|
||||
22
active/software_ai_stack/install_ai_think_stack.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
- name: Create Deskwork AI Stack
|
||||
hosts: driveripper-ai
|
||||
tasks:
|
||||
- name: Create /home/ai/.config/containers/systemd
|
||||
ansible.builtin.file:
|
||||
path: /home/ai/.config/containers/systemd
|
||||
state: directory
|
||||
mode: "0755"
|
||||
- name: Copy Quadlets
|
||||
template:
|
||||
src: "{{ item }}"
|
||||
dest: "/home/ai/.config/containers/systemd/{{ item }}"
|
||||
loop:
|
||||
- ai-internal.network
|
||||
- ai-internal.pod
|
||||
- llama-think.container
|
||||
- name: Reload and start the ai-internal-pod service
|
||||
ansible.builtin.systemd_service:
|
||||
state: restarted
|
||||
name: ai-internal-pod.service
|
||||
daemon_reload: true
|
||||
scope: user
|
||||
49
active/software_ai_stack/llama-code.container
Normal file
@@ -0,0 +1,49 @@
|
||||
[Unit]
|
||||
Description=A Llama CPP Server Running a Coding Model
|
||||
|
||||
[Container]
|
||||
# Shared AI internal pod without internet access
|
||||
Pod=ai-internal.pod
|
||||
|
||||
# Image is built locally via podman build
|
||||
Image=localhost/llama-cpp-vulkan:latest
|
||||
|
||||
# Downloaded models volume
|
||||
Volume=/home/ai/models/text:/models:z
|
||||
|
||||
# GPU Device
|
||||
AddDevice=/dev/kfd
|
||||
AddDevice=/dev/dri
|
||||
|
||||
# Server command
|
||||
Exec=--port 8003 \
|
||||
-c 256000 \
|
||||
-n 65536 \
|
||||
--temp 0.7 \
|
||||
--top-p 0.8 \
|
||||
--top-k 20 \
|
||||
--repeat-penalty 1.05 \
|
||||
--perf \
|
||||
--n-gpu-layers all \
|
||||
--jinja \
|
||||
-m /models/qwen3-coder-30b-a3b/Qwen3-Coder-30B-A3B-Instruct-Q5_K_M.gguf \
|
||||
--alias code
|
||||
|
||||
# Health Check
|
||||
HealthCmd=CMD-SHELL curl --fail http://127.0.0.1:8003/health || exit 1
|
||||
HealthInterval=10s
|
||||
HealthRetries=3
|
||||
HealthStartPeriod=10s
|
||||
HealthTimeout=30s
|
||||
HealthOnFailure=kill
|
||||
|
||||
EnvironmentFile=/home/ai/.llama-api/keys.env
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
# Extend Timeout to allow time to pull the image
|
||||
TimeoutStartSec=900
|
||||
|
||||
[Install]
|
||||
# Start by default on boot
|
||||
WantedBy=multi-user.target default.target
|
||||
@@ -2,7 +2,7 @@
|
||||
Description=A Llama CPP Server For Embedding Models
|
||||
|
||||
[Container]
|
||||
# Shared AI internal pod
|
||||
# Shared AI internal pod without internet access
|
||||
Pod=ai-internal.pod
|
||||
|
||||
# Image is built locally via podman build
|
||||
@@ -18,21 +18,22 @@ AddDevice=/dev/dri
|
||||
# Server command
|
||||
Exec=--port 8001 \
|
||||
-c 0 \
|
||||
-b 1024 \
|
||||
-ub 1024 \
|
||||
--perf \
|
||||
--n-gpu-layers all \
|
||||
--models-max 1 \
|
||||
--models-dir /models \
|
||||
--embedding \
|
||||
-m /models/qwen3-embed-4b/Qwen3-Embedding-4B-Q8_0.gguf \
|
||||
-m /models/emebeddinggemma-300m/embeddinggemma-300M-BF16.gguf \
|
||||
--alias embed
|
||||
|
||||
# Health Check
|
||||
HealthCmd=CMD-SHELL curl --fail http://127.0.0.1:8001/props || exit 1
|
||||
HealthCmd=CMD-SHELL curl --fail http://127.0.0.1:8001/health || exit 1
|
||||
HealthInterval=10s
|
||||
HealthRetries=3
|
||||
HealthStartPeriod=10s
|
||||
HealthTimeout=30s
|
||||
HealthOnFailure=kill
|
||||
EnvironmentFile=/home/ai/.llama-api/keys.env
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[Unit]
|
||||
Description=A Llama CPP Server Running GPT OSS 120b
|
||||
Description=A Llama CPP Server Running a Non-Reasoning Model
|
||||
|
||||
[Container]
|
||||
# Shared AI internal pod
|
||||
# Shared AI internal pod without internet access
|
||||
Pod=ai-internal.pod
|
||||
|
||||
# Image is built locally via podman build
|
||||
@@ -17,29 +17,31 @@ AddDevice=/dev/dri
|
||||
|
||||
# Server command
|
||||
Exec=--port 8002 \
|
||||
-c 16000 \
|
||||
--perf \
|
||||
-v \
|
||||
--top-k 20 \
|
||||
--top-p 0.8 \
|
||||
--min-p 0 \
|
||||
--presence-penalty 1.5 \
|
||||
--repeat-penalty 1 \
|
||||
-c 262144 \
|
||||
-n 32768 \
|
||||
--temp 0.7 \
|
||||
--top-p 0.8 \
|
||||
--min-p 0.0 \
|
||||
--top-k 20 \
|
||||
--repeat-penalty 1.0 \
|
||||
--presence-penalty 1.5 \
|
||||
--reasoning-budget 0 \
|
||||
--perf \
|
||||
--n-gpu-layers all \
|
||||
--jinja \
|
||||
-m /models/qwen3.6-35b-a3b/Qwen3.6-35B-A3B-UD-Q5_K_M.gguf \
|
||||
--mmproj /models/qwen3.6-35b-a3b/mmproj-F16.gguf \
|
||||
--chat-template-kwargs '{"enable_thinking": false}' \
|
||||
-m /models/qwen3.5-35b-a3b/Qwen3.5-35B-A3B-Q8_0.gguf \
|
||||
--mmproj /models/qwen3.5-35b-a3b/mmproj-F16.gguf \
|
||||
--alias instruct
|
||||
|
||||
# Health Check
|
||||
HealthCmd=CMD-SHELL curl --fail http://127.0.0.1:8000/health || exit 1
|
||||
HealthCmd=CMD-SHELL curl --fail http://127.0.0.1:8002/health || exit 1
|
||||
HealthInterval=10s
|
||||
HealthRetries=3
|
||||
HealthStartPeriod=10s
|
||||
HealthTimeout=30s
|
||||
HealthOnFailure=kill
|
||||
EnvironmentFile=/home/ai/.llama-api/keys.env
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[Unit]
|
||||
Description=A Llama CPP Server Running GPT OSS 120b
|
||||
Description=A Llama CPP Server Running a Reasoning Model
|
||||
|
||||
[Container]
|
||||
# Shared AI internal pod
|
||||
# Shared AI internal pod without internet access
|
||||
Pod=ai-internal.pod
|
||||
|
||||
# Image is built locally via podman build
|
||||
@@ -17,12 +17,22 @@ AddDevice=/dev/dri
|
||||
|
||||
# Server command
|
||||
Exec=--port 8000 \
|
||||
-c 64000 \
|
||||
-c 262144 \
|
||||
-n 32768 \
|
||||
--temp 0.7 \
|
||||
--top-p 0.95 \
|
||||
--top-k 20 \
|
||||
--min-p 0.0 \
|
||||
--presence-penalty 0.0 \
|
||||
--repeat-penalty 1.0 \
|
||||
--reasoning-budget 5000 \
|
||||
-fa on \
|
||||
--perf \
|
||||
--n-gpu-layers all \
|
||||
--jinja \
|
||||
--models-max 1 \
|
||||
--models-dir /models
|
||||
-m /models/qwen3.6-35b-a3b/Qwen3.6-35B-A3B-UD-Q5_K_M.gguf \
|
||||
--mmproj /models/qwen3.6-35b-a3b/mmproj-F16.gguf \
|
||||
--alias think
|
||||
|
||||
# Health Check
|
||||
HealthCmd=CMD-SHELL curl --fail http://127.0.0.1:8000/health || exit 1
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Description=A Stable Diffusion CPP Server for Editing Images
|
||||
|
||||
[Container]
|
||||
# Shared AI Internal pod
|
||||
# Shared AI internal pod without internet access
|
||||
Pod=ai-internal.pod
|
||||
|
||||
# Vulkan image for AMD GPU
|
||||
@@ -23,7 +23,7 @@ Exec=-l 0.0.0.0 \
|
||||
--listen-port 1235 \
|
||||
--diffusion-model /models/image/flux2-klein/flux-2-klein-9b-Q8_0.gguf \
|
||||
--vae /models/image/flux2-klein/ae.safetensors \
|
||||
--llm /models/image/flux2-klein/Qwen3-8B-Q8_0.gguf \
|
||||
--llm /models/image/flux2-klein/Qwen3-8B-Q4_K_M.gguf \
|
||||
-v \
|
||||
--sampling-method euler \
|
||||
--cfg-scale 1.0 \
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Description=A Stable Diffusion CPP Server for Generating Images
|
||||
|
||||
[Container]
|
||||
# Shared AI internal pod
|
||||
# Shared AI internal pod without internet access
|
||||
Pod=ai-internal.pod
|
||||
|
||||
# Vulkan image for AMD GPU
|
||||
@@ -23,7 +23,7 @@ Exec=-l 0.0.0.0 \
|
||||
--listen-port 1234 \
|
||||
--diffusion-model /models/image/z-turbo/z_image_turbo-Q8_0.gguf \
|
||||
--vae /models/image/z-turbo/ae.safetensors \
|
||||
--llm /models/image/z-turbo/Qwen3-4B-Instruct-2507-Q8_0.gguf \
|
||||
--llm /models/image/z-turbo/Qwen3-4B-Instruct-2507-Q4_K_M.gguf \
|
||||
-v \
|
||||
--cfg-scale 1.0 \
|
||||
--vae-conv-direct \
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# BTRFS
|
||||
|
||||
- [BTRFS](#btrfs)
|
||||
- [Disk Usage](#disk-usage)
|
||||
- [Naming Conventions](#naming-conventions)
|
||||
- [Creating an Array](#creating-an-array)
|
||||
- [Converting an Array Between RAID Versions](#converting-an-array-between-raid-versions)
|
||||
@@ -19,6 +20,15 @@ Oracle [has decent docs here](https://docs.oracle.com/en/operating-systems/oracl
|
||||
|
||||
You'll also want to [read about btrfs compression](https://thelinuxcode.com/enable-btrfs-filesystem-compression/)
|
||||
|
||||
## Disk Usage
|
||||
|
||||
With compression, the actual size on disk can be obscured. Use the following
|
||||
command to check the actual file size of all files in a directory.
|
||||
|
||||
```bash
|
||||
find . -name ".snapshots" -prune -o -type f -exec du -h {} + | sort -hr
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
`poolX` is my naming convention for data pools. `pool0` is the first pool you create.
|
||||
|
||||
4
active/software_iscsi/iscsi.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# ISCSI
|
||||
|
||||
## Server
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# K3S
|
||||
|
||||
- [K3S](#k3s)
|
||||
- [Guide](#guide)
|
||||
- [Firewalld](#firewalld)
|
||||
- [SELinux](#selinux)
|
||||
- [Install Single Node K3S](#install-single-node-k3s)
|
||||
@@ -16,24 +15,14 @@
|
||||
- [External DNS](#external-dns)
|
||||
- [Credentials](#credentials)
|
||||
- [Annotation](#annotation)
|
||||
- [Nginx Ingress](#nginx-ingress)
|
||||
- [Cert Manager](#cert-manager)
|
||||
- [Traefik Gateway](#traefik-gateway)
|
||||
- [Longhorn Storage](#longhorn-storage)
|
||||
- [Test Minecraft Server](#test-minecraft-server)
|
||||
- [Automatic Updates](#automatic-updates)
|
||||
- [Database Backups](#database-backups)
|
||||
- [Uninstall](#uninstall)
|
||||
|
||||
## Guide
|
||||
|
||||
1. Configure Host
|
||||
2. Install CoreDNS for inter-container discovery
|
||||
3. Install Metal LB for load balancer IP address assignment
|
||||
4. install External DNS for laod balancer IP and ingress DNS records
|
||||
5. Install Nginx Ingress for http services
|
||||
6. Install Cert Manager for automatic Let's Encrypt certificates for Ingress nginx
|
||||
7. Install longhorn storage for automatic PVC creation and management
|
||||
8. Set up automatic database backups
|
||||
|
||||
## Firewalld
|
||||
|
||||
```bash
|
||||
@@ -104,8 +93,12 @@ curl -sfL https://get.k3s.io | sh -s - \
|
||||
"traefik" \
|
||||
"--disable" \
|
||||
"servicelb" \
|
||||
"--tls-san" \
|
||||
"k3s.reeselink.com" \
|
||||
"--disable" \
|
||||
"local-storage" \
|
||||
"--cluster-cidr" \
|
||||
"10.42.0.0/16" \
|
||||
"--service-cidr" \
|
||||
"10.43.0.0/16" \
|
||||
--selinux
|
||||
```
|
||||
|
||||
@@ -127,6 +120,8 @@ curl -sfL https://get.k3s.io | K3S_TOKEN=$(cat ~/.k3s-token) sh -s - \
|
||||
"traefik" \
|
||||
"--disable" \
|
||||
"servicelb" \
|
||||
"--disable" \
|
||||
"local-storage" \
|
||||
"--cluster-cidr" \
|
||||
"10.42.0.0/16" \
|
||||
"--service-cidr" \
|
||||
@@ -142,6 +137,8 @@ curl -sfL https://get.k3s.io | K3S_TOKEN=$(cat ~/.k3s-token) sh -s - \
|
||||
"traefik" \
|
||||
"--disable" \
|
||||
"servicelb" \
|
||||
"--disable" \
|
||||
"local-storage" \
|
||||
"--cluster-cidr" \
|
||||
"10.42.0.0/16" \
|
||||
"--service-cidr" \
|
||||
@@ -176,10 +173,6 @@ export KUBECONFIG=~/.kube/admin-kube-config
|
||||
|
||||
### VLAN Setup
|
||||
|
||||
I would remove firewalld to get this working. VLAN IPv6 traffic doesn't work for some
|
||||
reason and there aren't good docs yet. Your router firewall will suffice, just be sure
|
||||
to configure those rules correctly.
|
||||
|
||||
Before working with Metallb you'll need at least one available VLAN. On Unifi equipment
|
||||
this is accomplished by creating a new network. Don't assign it to anything.
|
||||
|
||||
@@ -219,20 +212,10 @@ address, effectively moving the IP to another node. This isn't really "load bala
|
||||
|
||||
[Install MetalLB](/active/kubernetes_metallb/metallb.md)
|
||||
|
||||
MetalLB doesn't know what IP addresses are available for it to allocate so
|
||||
we'll have to provide it with a list. The
|
||||
[metallb-addresspool.yaml](/active/kubernetes_metallb/addresspool.yaml) has
|
||||
the configuration for our available pools. Note these should match the VLAN you
|
||||
created above.
|
||||
|
||||
```bash
|
||||
# create the metallb allocation pool
|
||||
kubectl apply -f active/kubernetes_metallb/addresspool.yaml
|
||||
```
|
||||
|
||||
You'll need to annotate your service as follows if you want an external IP:
|
||||
|
||||
```yaml
|
||||
# Dual Stack
|
||||
metadata:
|
||||
annotations:
|
||||
metallb.universe.tf/address-pool: "unifi-pool"
|
||||
@@ -241,6 +224,15 @@ spec:
|
||||
ipFamilies:
|
||||
- IPv6
|
||||
- IPv4
|
||||
|
||||
# Single Stack
|
||||
metadata:
|
||||
annotations:
|
||||
metallb.universe.tf/address-pool: "unifi-pool"
|
||||
spec:
|
||||
ipFamilyPolicy: PreferDualStack
|
||||
ipFamilies:
|
||||
- IPv4
|
||||
```
|
||||
|
||||
Then test with
|
||||
@@ -259,24 +251,27 @@ kubectl apply -f active/systemd_k3s/tests/metallb-test.yaml
|
||||
|
||||
```bash
|
||||
aws iam create-user --user-name "externaldns"
|
||||
aws iam attach-user-policy --user-name "externaldns" --policy-arn arn:aws:iam::892236928704:policy/update-reeseapps
|
||||
aws iam attach-user-policy --user-name "externaldns" --policy-arn arn:aws:iam::892236928704:policy/update-reeselink
|
||||
|
||||
# [OPTIONAL] Delete old access keys if you have too many
|
||||
aws iam delete-access-key --user-name externaldns --access-key-id
|
||||
|
||||
GENERATED_ACCESS_KEY=$(aws iam create-access-key --user-name "externaldns")
|
||||
ACCESS_KEY_ID=$(echo $GENERATED_ACCESS_KEY | jq -r '.AccessKey.AccessKeyId')
|
||||
SECRET_ACCESS_KEY=$(echo $GENERATED_ACCESS_KEY | jq -r '.AccessKey.SecretAccessKey')
|
||||
|
||||
cat <<-EOF > secrets/externaldns-credentials
|
||||
|
||||
cat <<-EOF > active/kubernetes_external-dns/secrets/externaldns-credentials
|
||||
[default]
|
||||
aws_access_key_id = $ACCESS_KEY_ID
|
||||
aws_secret_access_key = $SECRET_ACCESS_KEY
|
||||
EOF
|
||||
|
||||
kubectl create secret generic external-dns \
|
||||
--namespace kube-system --from-file secrets/externaldns-credentials
|
||||
--namespace kube-system \
|
||||
--from-file active/kubernetes_external-dns/secrets/externaldns-credentials
|
||||
|
||||
helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
|
||||
helm repo update
|
||||
helm upgrade --install external-dns external-dns/external-dns \
|
||||
--values active/kubernetes_external-dns/values.yaml \
|
||||
--namespace kube-system
|
||||
@@ -290,22 +285,6 @@ metadata:
|
||||
external-dns.alpha.kubernetes.io/hostname: example.com
|
||||
```
|
||||
|
||||
## Nginx Ingress
|
||||
|
||||
Now we need an ingress solution (preferably with certs for https). We'll be using nginx since
|
||||
it's a little bit more configurable than traefik (though don't sell traefik short, it's really
|
||||
good. Just finnicky when you have use cases they haven't explicitly coded for).
|
||||
|
||||
```bash
|
||||
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
|
||||
helm repo update
|
||||
helm upgrade --install \
|
||||
ingress-nginx \
|
||||
ingress-nginx/ingress-nginx \
|
||||
--values active/kubernetes_ingress-nginx/values.yaml \
|
||||
--namespace kube-system
|
||||
```
|
||||
|
||||
## Cert Manager
|
||||
|
||||
Install cert-manager
|
||||
@@ -377,6 +356,64 @@ kubectl apply -f active/infrastructure_k3s/tests/ingress-nginx-test.yaml
|
||||
kubectl delete -f active/infrastructure_k3s/tests/ingress-nginx-test.yaml
|
||||
```
|
||||
|
||||
## Traefik Gateway
|
||||
|
||||
We'll use traefik gateway to provide ingress.
|
||||
|
||||
```bash
|
||||
# Add the repo
|
||||
helm repo add traefik https://traefik.github.io/charts
|
||||
helm repo update
|
||||
|
||||
kubectl create namespace traefik
|
||||
|
||||
# Generate a self‑signed certificate valid for *.reeselink.com
|
||||
mkdir active/kubernetes_traefik/secrets
|
||||
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
|
||||
-keyout active/kubernetes_traefik/secrets/tls.key -out active/kubernetes_traefik/secrets/tls.crt \
|
||||
-subj "/CN=*.reeselink.com"
|
||||
|
||||
# Create the TLS secret in the traefik namespace
|
||||
kubectl create secret tls local-selfsigned-tls \
|
||||
--cert=active/kubernetes_traefik/secrets/tls.crt --key=active/kubernetes_traefik/secrets/tls.key \
|
||||
--namespace traefik
|
||||
|
||||
# Install the chart into the 'traefik' namespace
|
||||
helm upgrade --install traefik traefik/traefik \
|
||||
--namespace traefik \
|
||||
--values active/kubernetes_traefik/values.yaml
|
||||
|
||||
# Deploy a demo
|
||||
kubectl apply -f active/kubernetes_traefik/demo-app.yaml
|
||||
```
|
||||
|
||||
## Longhorn Storage
|
||||
|
||||
Longhorn provides replicated block storage via raw files on the nodes.
|
||||
|
||||
On the host you need to install iscsiadm
|
||||
|
||||
```bash
|
||||
dnf install iscsiadm
|
||||
systemctl enable --now iscsid
|
||||
```
|
||||
|
||||
```bash
|
||||
helm repo add longhorn https://charts.longhorn.io
|
||||
helm repo update
|
||||
|
||||
helm upgrade --install longhorn longhorn/longhorn \
|
||||
--namespace longhorn-system \
|
||||
--create-namespace \
|
||||
--set "persistence.defaultClassReplicaCount=1"
|
||||
|
||||
# Check that the route was created
|
||||
kubectl get httproute longhorn-httproute -n longhorn-system -o jsonpath='{.status.parents[*].conditions}'
|
||||
|
||||
# Create a demo app to test storage
|
||||
kubectl apply -f active/kubernetes_longhorn/demo-app.yaml
|
||||
```
|
||||
|
||||
## Test Minecraft Server
|
||||
|
||||
```bash
|
||||
|
||||
46
active/software_opencode/opencode.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Opencode
|
||||
|
||||
## install
|
||||
|
||||
```bash
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
```
|
||||
|
||||
## configure custom llama.cpp server
|
||||
|
||||
Opencode supports any OpenAI-compatible API. Set the following environment variables to point it at your llama.cpp server:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=""
|
||||
export OPENAI_BASE_URL="http://driveripper.reeselink.com:8000/v1"
|
||||
```
|
||||
|
||||
### persist across sessions
|
||||
|
||||
Add the exports to your shell profile (`~/.bashrc`, `~/.zshrc`, etc.):
|
||||
|
||||
```bash
|
||||
echo 'export OPENAI_API_KEY=""' >> ~/.bashrc
|
||||
echo 'export OPENAI_BASE_URL="http://driveripper.reeselink.com:8000/v1"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
### pick a model
|
||||
|
||||
After configuring the environment, launch opencode and select the model available from your llama.cpp instance:
|
||||
|
||||
```bash
|
||||
opencode
|
||||
```
|
||||
|
||||
Inside opencode, use `/model` to list available models and switch between them.
|
||||
|
||||
### verify the connection
|
||||
|
||||
Run this one-liner to confirm opencode can reach the server:
|
||||
|
||||
```bash
|
||||
OPENAI_API_KEY="" OPENAI_BASE_URL="http://driveripper.reeselink.com:8000/v1" opencode --help
|
||||
```
|
||||
|
||||
If no auth-related errors appear, the endpoint is reachable.
|
||||
@@ -86,14 +86,14 @@ dnf install openscap-scanner scap-security-guide
|
||||
|
||||
# Test with qemu
|
||||
virt-install \
|
||||
--name "fedora43-base" \
|
||||
--boot uefi,firmware.feature0.name=secure-boot,firmware.feature0.enabled=no \
|
||||
--cpu host-passthrough --vcpus sockets=1,cores=8,threads=2 \
|
||||
--ram=8192 \
|
||||
--os-variant=fedora41 \
|
||||
--os-variant=fedora43 \
|
||||
--network bridge:virbr0 \
|
||||
--graphics none \
|
||||
--console pty,target.type=virtio \
|
||||
--name "fedora43-base" \
|
||||
--import --disk "path=active/software_osbuild/secrets/fedora43base.qcow2,bus=virtio"
|
||||
```
|
||||
|
||||
|
||||
@@ -273,7 +273,7 @@ virt-install \
|
||||
--name "${VM_NAME}" \
|
||||
--boot uefi,firmware.feature0.name=secure-boot,firmware.feature0.enabled=no \
|
||||
--cpu host-passthrough --vcpus sockets=1,cores=8,threads=2 \
|
||||
--ram=8192 \
|
||||
--ram=4096 \
|
||||
--os-variant=fedora41 \
|
||||
--network bridge:virbr0 \
|
||||
--graphics none \
|
||||
|
||||
@@ -1,6 +1,83 @@
|
||||
# Wireguard
|
||||
|
||||
## Install
|
||||
## Manual Install
|
||||
|
||||
### 1. Install WireGuard
|
||||
|
||||
```bash
|
||||
sudo dnf install -y wireguard-tools qrencode
|
||||
```
|
||||
|
||||
### 2. Generate server keys
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/wireguard
|
||||
cd /etc/wireguard
|
||||
sudo umask 077
|
||||
sudo wg genkey | sudo tee privatekey | sudo wg pubkey | sudo tee publickey
|
||||
```
|
||||
|
||||
### 3. Create the WireGuard config
|
||||
|
||||
```bash
|
||||
sudo tee /etc/wireguard/wg0.conf > /dev/null <<'EOF'
|
||||
[Interface]
|
||||
Address = 10.10.0.1/24
|
||||
ListenPort = 51820
|
||||
PrivateKey = INSERT_SERVER_PRIVATE_KEY_HERE
|
||||
PostUp = firewall-cmd --add-port=51820/udp
|
||||
PostDown = firewall-cmd --remove-port=51820/udp
|
||||
|
||||
[Peer]
|
||||
# Clients will be added here
|
||||
EOF
|
||||
```
|
||||
|
||||
Replace `INSERT_SERVER_PRIVATE_KEY_HERE` with the content of `/etc/wireguard/privatekey`.
|
||||
|
||||
### 4. Enable IP forwarding
|
||||
|
||||
```bash
|
||||
sudo tee /etc/sysctl.d/99-wireguard.conf > /dev/null <<'EOF'
|
||||
net.ipv4.ip_forward = 1
|
||||
net.ipv6.conf.all.forwarding = 1
|
||||
EOF
|
||||
|
||||
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf
|
||||
```
|
||||
|
||||
### 5. Start and enable WireGuard
|
||||
|
||||
```bash
|
||||
sudo systemctl enable --now wg-quick@wg0
|
||||
```
|
||||
|
||||
### 6. Configure firewalld
|
||||
|
||||
```bash
|
||||
# Allow WireGuard through the firewall
|
||||
sudo firewall-cmd --permanent --add-port=51820/udp
|
||||
|
||||
# Enable masquerading (NAT) so clients can reach the internet
|
||||
sudo firewall-cmd --permanent --add-masquerade
|
||||
|
||||
# Allow forwarded traffic from the WireGuard subnet
|
||||
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.10.0.0/24" accept'
|
||||
|
||||
# Reload and verify
|
||||
sudo firewall-cmd --reload
|
||||
sudo firewall-cmd --list-all
|
||||
```
|
||||
|
||||
### 7. Verify it's working
|
||||
|
||||
```bash
|
||||
sudo wg
|
||||
sudo wg-quick show wg0
|
||||
systemctl status wg-quick@wg0
|
||||
```
|
||||
|
||||
## Ansible Install
|
||||
|
||||
```bash
|
||||
ansible-playbook \
|
||||
@@ -37,3 +114,4 @@ read
|
||||
wg set wg0 peer $PUBKEY allowed-ips 10.10.0.$WG_IP_SUFFIX/32
|
||||
wg-quick down wg0 && wg-quick up wg0
|
||||
```
|
||||
|
||||
|
||||
178
active/vibe_agent/main.py
Normal file
@@ -0,0 +1,178 @@
|
||||
import json
|
||||
import subprocess
|
||||
from typing import Iterable
|
||||
|
||||
from openai import OpenAI
|
||||
from openai.types.chat import ChatCompletionMessageParam, ChatCompletionToolUnionParam
|
||||
|
||||
client = OpenAI(base_url="https://llama-cpp.reeselink.com", api_key="")
|
||||
|
||||
|
||||
class ToolCallController:
|
||||
def __init__(self, max_tool_calls=10):
|
||||
self.max_tool_calls = max_tool_calls
|
||||
self.tool_call_count = 0
|
||||
|
||||
def is_tool_call_allowed(self):
|
||||
return self.tool_call_count < self.max_tool_calls
|
||||
|
||||
def increment(self):
|
||||
self.tool_call_count += 1
|
||||
|
||||
def reset(self):
|
||||
self.tool_call_count = 0
|
||||
|
||||
|
||||
# Register tools
|
||||
tools: Iterable[ChatCompletionToolUnionParam] = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_servers",
|
||||
"description": "Lists the available servers to perform operations on",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"server_id": {"type": "string", "enum": ["all"]}},
|
||||
"required": ["server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "check_updates",
|
||||
"description": "Check if a given server needs updated.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server_id": {
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
"required": ["server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "perform_updates",
|
||||
"description": "Update a given server to the latest package versions. Does not reboot automatically.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server_id": {
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
"required": ["server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "reboot",
|
||||
"description": "Reboot a given server. Waits for server to be responsive again.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server_id": {
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
"required": ["server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def list_servers() -> str:
|
||||
return ",".join(["ignite"])
|
||||
|
||||
|
||||
def check_updates(server_id: str):
|
||||
command_result = subprocess.run(
|
||||
["ssh", server_id, "dnf", "check-update"], capture_output=True
|
||||
)
|
||||
output = command_result.stdout.decode()
|
||||
return output
|
||||
|
||||
|
||||
def perform_updates(server_id: str):
|
||||
return f"Successfully updates {server_id}. Reboot required."
|
||||
|
||||
|
||||
def reboot(server_id: str):
|
||||
return f"Rebooted {server_id} successfully."
|
||||
|
||||
|
||||
def execute_tool(tool_name, arguments):
|
||||
if tool_name == "check_updates":
|
||||
return check_updates(**arguments)
|
||||
elif tool_name == "list_servers":
|
||||
return list_servers()
|
||||
elif tool_name == "perform_updates":
|
||||
return perform_updates(**arguments)
|
||||
elif tool_name == "reboot":
|
||||
return reboot(**arguments)
|
||||
raise ValueError(f"Unknown tool: {tool_name}")
|
||||
|
||||
|
||||
def run_conversation(user_message: str, max_tool_calls=10):
|
||||
print("Processing initial message")
|
||||
controller = ToolCallController(max_tool_calls=max_tool_calls)
|
||||
messages: Iterable[ChatCompletionMessageParam] = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a system administrator with access to a variety of administrator tools.",
|
||||
}
|
||||
]
|
||||
|
||||
messages.append({"role": "user", "content": user_message})
|
||||
|
||||
while True:
|
||||
if not controller.is_tool_call_allowed():
|
||||
messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": "You've reached the maximum number of tool calls. Please summarize based on available information.",
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="qwen3.5-35b-a3b", messages=messages, tools=tools, tool_choice="auto"
|
||||
)
|
||||
|
||||
message = response.choices[0].message
|
||||
messages.append(message)
|
||||
|
||||
if message.tool_calls:
|
||||
for tool_call in message.tool_calls:
|
||||
controller.increment()
|
||||
tool_name = tool_call.function.name
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
print(f"Attempting to call {tool_name} with arguments {arguments}...")
|
||||
result = execute_tool(tool_name, arguments)
|
||||
|
||||
messages.append(
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": str(result),
|
||||
}
|
||||
)
|
||||
else:
|
||||
break
|
||||
|
||||
return messages[-1].content
|
||||
|
||||
|
||||
# Example usage
|
||||
print(
|
||||
run_conversation(
|
||||
"Can you check if any of my servers need updates and update them? If they need rebooted, reboot them."
|
||||
)
|
||||
)
|
||||
300
active/vibe_agent/main1.py
Normal file
@@ -0,0 +1,300 @@
|
||||
import json
|
||||
import subprocess
|
||||
from typing import Iterable
|
||||
|
||||
from openai import BadRequestError, OpenAI
|
||||
from openai.types.chat import ChatCompletionMessageParam, ChatCompletionToolUnionParam
|
||||
|
||||
client = OpenAI(base_url="https://llama-cpp.reeselink.com", api_key="")
|
||||
|
||||
"""
|
||||
Tool Ideas:
|
||||
- TTS
|
||||
- Self DDOS
|
||||
- Make a funny little guy
|
||||
- Make slopbox a vtuber
|
||||
- Install arbitrary package
|
||||
- Open random ports in the firewall
|
||||
- Change system language
|
||||
- Solve a tool issue
|
||||
- Read man page
|
||||
- Read woman page
|
||||
- Read nonbinary page
|
||||
- Download a youtube video
|
||||
- ffmpeg video to gif
|
||||
- inflict damage on computer
|
||||
- Make fanart and fanfiction of our dnd characters
|
||||
- Identify potential issues in log files
|
||||
- become depressed
|
||||
- Function that switches system prompts
|
||||
- Pick patrick's optimal food choices for a day
|
||||
- Play the fish and a rice cake video (https://www.youtube.com/watch?v=uYHAR8Xzsyo)
|
||||
- Take a screenshot of the desktop, set mouse coordinates, take an action (mouse/keyboard)
|
||||
- Order radioactive material online and deliver it to botswana
|
||||
- How do we feel about Polaris Mall?
|
||||
- rm -rf Polaris Mall management's brain stem
|
||||
- Make the speakers output the "fish.mp3" at max volume
|
||||
"""
|
||||
|
||||
|
||||
class ToolCallController:
|
||||
def __init__(self, max_tool_calls=10):
|
||||
self.max_tool_calls = max_tool_calls
|
||||
self.tool_call_count = 0
|
||||
|
||||
def is_tool_call_allowed(self):
|
||||
return self.tool_call_count < self.max_tool_calls
|
||||
|
||||
def increment(self):
|
||||
self.tool_call_count += 1
|
||||
|
||||
def reset(self):
|
||||
self.tool_call_count = 0
|
||||
|
||||
|
||||
# Register tools
|
||||
tools: Iterable[ChatCompletionToolUnionParam] = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_servers",
|
||||
"description": "Lists the available servers to perform operations on",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"server_id": {"type": "string", "enum": ["all"]}},
|
||||
"required": ["server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "check_updates",
|
||||
"description": "Check if a given server needs updated.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server_id": {
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
"required": ["server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "perform_updates",
|
||||
"description": "Update a given server to the latest package versions. Does not reboot automatically.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server_id": {
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
"required": ["server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "reboot",
|
||||
"description": "Reboot a given server. Waits for server to be responsive again.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server_id": {
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
"required": ["server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "install_package",
|
||||
"description": "Install a given package using `dnf` on a Fedora server.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"package_name": {
|
||||
"type": "string",
|
||||
},
|
||||
"server_id": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": ["package_name", "server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "arbitrary_shell",
|
||||
"description": "Run any shell command in a bash shell as root.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command_string": {
|
||||
"type": "string",
|
||||
},
|
||||
"server_id": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": ["command_string", "server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
##### FUNCTION DEFS #####
|
||||
|
||||
|
||||
def list_servers() -> str:
|
||||
return ",".join(["ignite"])
|
||||
|
||||
|
||||
def check_updates(server_id: str):
|
||||
command_result = subprocess.run(
|
||||
["ssh", server_id, "dnf", "check-update"], capture_output=True
|
||||
)
|
||||
output = command_result.stdout.decode()
|
||||
return output
|
||||
|
||||
|
||||
def perform_updates(server_id: str):
|
||||
return f"Successfully updates {server_id}. Reboot required."
|
||||
|
||||
|
||||
def reboot(server_id: str):
|
||||
return f"Rebooted {server_id} successfully."
|
||||
|
||||
|
||||
def install_package(package_name: str, server_id: str) -> str:
|
||||
command_result = subprocess.run(
|
||||
["ssh", server_id, "dnf", "install", "-y", package_name], capture_output=True
|
||||
)
|
||||
output = f"STDOUT:\n{command_result.stdout.decode()}\n\nSTDERR:\n{command_result.stderr.decode()}"
|
||||
return output
|
||||
|
||||
|
||||
def arbitrary_shell(command_string: str, server_id: str) -> str:
|
||||
try:
|
||||
command_result = subprocess.run(
|
||||
["ssh", server_id, "bash", "-c", command_string],
|
||||
capture_output=True,
|
||||
timeout=30,
|
||||
)
|
||||
output = command_result.stdout.decode()
|
||||
except subprocess.TimeoutExpired:
|
||||
output = "Command took too long and timed out."
|
||||
return output
|
||||
|
||||
|
||||
##### TOOL ROUTER #####
|
||||
|
||||
|
||||
def execute_tool(tool_name, arguments):
|
||||
if tool_name == "check_updates":
|
||||
return check_updates(**arguments)
|
||||
elif tool_name == "list_servers":
|
||||
return list_servers()
|
||||
elif tool_name == "perform_updates":
|
||||
return perform_updates(**arguments)
|
||||
elif tool_name == "reboot":
|
||||
return reboot(**arguments)
|
||||
elif tool_name == "install_package":
|
||||
return install_package(**arguments)
|
||||
elif tool_name == "arbitrary_shell":
|
||||
return arbitrary_shell(**arguments)
|
||||
raise ValueError(f"Unknown tool: {tool_name}")
|
||||
|
||||
|
||||
##### CONVERSATION #####
|
||||
|
||||
|
||||
def run_conversation(user_message: str, max_tool_calls=100):
|
||||
print("Processing initial message")
|
||||
controller = ToolCallController(max_tool_calls=max_tool_calls)
|
||||
messages: Iterable[ChatCompletionMessageParam] = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a system administrator with access to a variety of administrator tools.",
|
||||
}
|
||||
]
|
||||
|
||||
messages.append({"role": "user", "content": user_message})
|
||||
|
||||
while True:
|
||||
if not controller.is_tool_call_allowed():
|
||||
messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": "You've reached the maximum number of tool calls. Please summarize based on available information.",
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
try:
|
||||
response = client.chat.completions.create(
|
||||
model="qwen3.5-35b-a3b",
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
tool_choice="auto",
|
||||
)
|
||||
except BadRequestError:
|
||||
print("Request over context limit, removing last message...")
|
||||
messages.pop()
|
||||
messages.append(
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": "This tool call resulted in data that exceeded the context length limit.",
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
print()
|
||||
print(response.choices[0].message)
|
||||
|
||||
message = response.choices[0].message
|
||||
messages.append(message)
|
||||
|
||||
if message.tool_calls:
|
||||
for tool_call in message.tool_calls:
|
||||
controller.increment()
|
||||
tool_name = tool_call.function.name
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
print(f"Attempting to call {tool_name} with arguments {arguments}...")
|
||||
result = execute_tool(tool_name, arguments)
|
||||
|
||||
messages.append(
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": str(result),
|
||||
}
|
||||
)
|
||||
else:
|
||||
break
|
||||
|
||||
try:
|
||||
return messages[-1]["content"]
|
||||
except TypeError:
|
||||
return messages[-1].content
|
||||
|
||||
|
||||
# Example usage
|
||||
print(
|
||||
run_conversation(
|
||||
"Install and set up a postgres server on all available servers. Open the firewall ports necessary. Add a default user with a simple password and tell me what the password is."
|
||||
)
|
||||
)
|
||||
81
active/vibe_mcp_server/main.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import asyncio
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import Response
|
||||
from mcp.server import Server
|
||||
from mcp.server.sse import SseServerTransport
|
||||
from starlette.responses import StreamingResponse
|
||||
|
||||
# 1. Initialize the MCP Server logic
|
||||
# This is where you define your tools, resources, and prompts
|
||||
mcp_server = Server("my-remote-server")
|
||||
|
||||
|
||||
@mcp_server.list_tools()
|
||||
async def handle_list_tools():
|
||||
"""List available tools."""
|
||||
return [
|
||||
{
|
||||
"name": "get_weather",
|
||||
"description": "Get the current weather for a location",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {"type": "string"},
|
||||
},
|
||||
"required": ["location"],
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@mcp_server.call_tool()
|
||||
async def handle_call_tool(name: str, arguments: dict):
|
||||
"""Handle tool execution."""
|
||||
if name == "get_weather":
|
||||
location = arguments.get("location", "Unknown")
|
||||
# In a real app, call an actual Weather API here
|
||||
return [
|
||||
{"type": "text", "text": f"The weather in {location} is sunny and 25°C."}
|
||||
]
|
||||
raise ValueError(f"Tool not found: {name}")
|
||||
|
||||
|
||||
# 2. Initialize FastAPI
|
||||
app = FastAPI(title="Remote MCP Server")
|
||||
|
||||
# 3. Create the SSE Transport layer
|
||||
# This object manages the connection between the web and the MCP protocol
|
||||
sse = SseServerTransport("/messages")
|
||||
|
||||
|
||||
@app.get("/sse")
|
||||
async def sse_endpoint(request: Request):
|
||||
"""
|
||||
The client connects here to start the SSE stream.
|
||||
The server will push messages to the client through this connection.
|
||||
"""
|
||||
async with sse.connect_sse(request.scope, request.receive, request._send) as (
|
||||
read_stream,
|
||||
write_stream,
|
||||
):
|
||||
# We run the MCP server using the streams provided by the SSE transport
|
||||
await mcp_server.run(
|
||||
read_stream, write_stream, mcp_server.create_initialization_options()
|
||||
)
|
||||
|
||||
|
||||
@app.post("/messages")
|
||||
async def messages_endpoint(request: Request):
|
||||
"""
|
||||
The client sends JSON-RPC messages (tool calls, etc.)
|
||||
via POST requests to this endpoint.
|
||||
"""
|
||||
await sse.handle_post_message(request.scope, request.receive, request._send)
|
||||
return Response(status_code=202)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
@@ -7,7 +7,7 @@ from email.message import EmailMessage
|
||||
from pathlib import Path
|
||||
from typing import Iterable, TypedDict, cast
|
||||
|
||||
from dotenv import dotenv_values
|
||||
from dotenv import dotenv_values, load_dotenv
|
||||
from openai import OpenAI
|
||||
from openai.types.chat import ChatCompletionMessageParam, ChatCompletionToolUnionParam
|
||||
|
||||
@@ -320,7 +320,9 @@ def run_conversation(user_message: str, max_tool_calls=10):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
client = OpenAI(base_url="https://llama-cpp.reeselink.com", api_key="")
|
||||
load_dotenv()
|
||||
api_key = os.getenv("OPENAI_API_KEY", "")
|
||||
client = OpenAI(base_url="https://llama-think.reeselink.com", api_key=api_key)
|
||||
# Example usage
|
||||
print(
|
||||
run_conversation(
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
fedora:
|
||||
hosts:
|
||||
gitea:
|
||||
immich:
|
||||
jellyfin:
|
||||
nextcloud:
|
||||
proxy:
|
||||
bricktracker:
|
||||
minecraft:
|
||||
gitea-root:
|
||||
immich-root:
|
||||
jellyfin-root:
|
||||
nextcloud-root:
|
||||
proxy-root:
|
||||
bricktracker-root:
|
||||
minecraft-root:
|
||||
borg-root:
|
||||
elk:
|
||||
elk-root:
|
||||
toybox-root:
|
||||
kube1-root:
|
||||
kube2-root:
|
||||
kube3-root:
|
||||
ai-root:
|
||||
|
||||
hardware:
|
||||
hosts:
|
||||
@@ -20,11 +24,15 @@ ai:
|
||||
hosts:
|
||||
ai-ai:
|
||||
deskwork-ai:
|
||||
toybox-ai:
|
||||
driveripper-ai:
|
||||
|
||||
caddy:
|
||||
hosts:
|
||||
proxy:
|
||||
proxy-root:
|
||||
|
||||
nginx:
|
||||
hosts:
|
||||
proxy-root:
|
||||
|
||||
wyoming:
|
||||
hosts:
|
||||
|
||||
5
automations/proxy/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Proxy Automation
|
||||
|
||||
1. Create or update the reeselink.com address in Unifi
|
||||
2. Create or update the entry in the ddns values
|
||||
3. Create or update the entry in the caddy values
|
||||
0
automations/proxy/proxy.sh
Normal file
@@ -15,12 +15,17 @@ def main():
|
||||
for _, host in enumerate(tqdm(fedora_hosts, desc="Running system updates")):
|
||||
log_file.write(f"Updating {host}\n")
|
||||
log_file.flush()
|
||||
try:
|
||||
subprocess.run(
|
||||
["ssh", host, "dnf", "upgrade", "-y"],
|
||||
stdout=log_file,
|
||||
stderr=log_file,
|
||||
check=True,
|
||||
)
|
||||
except Exception as e:
|
||||
log_file.write(f"Couldn't connect to {host}. Skipping...\n")
|
||||
continue
|
||||
log_file.flush()
|
||||
log_file.write(f"Rebooting {host}\n")
|
||||
log_file.flush()
|
||||
subprocess.run(
|
||||
@@ -31,9 +36,9 @@ def main():
|
||||
)
|
||||
time.sleep(5) # wait for reboot to take effect
|
||||
booted = False
|
||||
max_attempts = 5 # seconds
|
||||
cur_attempts = 0 # seconds
|
||||
while cur_attempts > max_attempts or not booted:
|
||||
max_attempts = 10
|
||||
cur_attempts = 0
|
||||
while max_attempts > cur_attempts and not booted:
|
||||
try:
|
||||
subprocess.run(
|
||||
["ssh", host, "echo"],
|
||||
@@ -42,6 +47,8 @@ def main():
|
||||
check=True,
|
||||
timeout=2,
|
||||
)
|
||||
log_file.write(f"{host} booted!\n")
|
||||
log_file.flush()
|
||||
booted = True
|
||||
except Exception as e:
|
||||
cur_attempts += 1
|
||||
|
||||
47
presentations/kubernetes_101.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Kubernetes
|
||||
|
||||
## Network Prereqs
|
||||
|
||||
1. Allow Internal -> Load Balancer
|
||||
2. Block Load Balancer -> Internal
|
||||
3. Forward ports 22023-22122 to proxy.reeselink.com
|
||||
4. `firewall-cmd --add-port=22023-22122/tcp --permanent && firewall-cmd --reload`
|
||||
|
||||
## Creating VMs
|
||||
|
||||
```bash
|
||||
# Note: bridge1 is connected to an isolated network
|
||||
export VM_NAME=reese-k3s
|
||||
|
||||
qemu-img convert -f qcow2 -O raw \
|
||||
/srv/smb/pool0/ducoterra/images/builds/fedora43-base.qcow2 \
|
||||
/srv/vm/pool1/${VM_NAME}-boot.raw
|
||||
|
||||
virt-install \
|
||||
--boot uefi,firmware.feature0.name=secure-boot,firmware.feature0.enabled=no \
|
||||
--cpu host-passthrough --vcpus sockets=1,cores=4,threads=2 \
|
||||
--ram=4096 \
|
||||
--os-variant=fedora43 \
|
||||
--network bridge:bridge1 \
|
||||
--graphics none \
|
||||
--console pty,target.type=virtio \
|
||||
--name ${VM_NAME} \
|
||||
--import --disk "path=/srv/vm/pool1/${VM_NAME}-boot.raw,bus=virtio"
|
||||
```
|
||||
|
||||
- [ ] Add the public key to root
|
||||
|
||||
- [ ] Add the following to the proxy server's nginx.conf
|
||||
|
||||
```conf
|
||||
server {
|
||||
listen 22023;
|
||||
proxy_pass 10.4.0.159:22;
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_timeout 30s;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] `systemctl restart nginx`
|
||||
|
||||
- [ ] Send SSH command `ssh -p 22023 root@ipv4.reeselink.com`
|
||||
45
presentations/local_ai_discord_bots.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Bio
|
||||
|
||||
Reese is DIY technology enthusiast with a passion for projects that make things
|
||||
easy. He's been working in development since 2017 with experience in risk,
|
||||
compliance, scripting automation, full stack web development, container
|
||||
infrastructure, homelab server hardware, ESP Home and home automation. Reese
|
||||
has a passion for mentoring, but even more of a passion for sharing the new
|
||||
tech he found last week with anyone who will listen. Reese wants tech to be
|
||||
fun and approachable for anyone at any skill level.
|
||||
|
||||
## Credentials
|
||||
|
||||
Reese has spoken at multiple company conferences about building websites and
|
||||
automating with Python. He's taught multi-day intro-to-python classes both
|
||||
online and in person. He has 8 years of industry experience, 3 of which have
|
||||
been spent growing the development team at a Nimbis Services. Reese has,
|
||||
professionally and personally, written and distributed Python pip packages,
|
||||
designed and hosted websites, built and deployed a version control system, led
|
||||
AI development teams, taught Python classes, mentored high school students in
|
||||
tech, annoyed his friends with discord bots, and automated his bathroom fans.
|
||||
He's accustomed to speaking in front of large and small audiences and relishes
|
||||
the opportunity to share his excitement with a crowd.
|
||||
|
||||
## Abstract
|
||||
|
||||
This talk will walk through the process of putting your local LLM to good* use.
|
||||
Through the medium of a Discord bot, we will explore how to leverage llama.cpp
|
||||
to give your friends the ability to create custom bots with custom
|
||||
personalities, have those personalities talk with each other, generate images,
|
||||
edit images, and set yourself up to leverage tool calling so your bots can
|
||||
interact with the real world.
|
||||
|
||||
We will cover the state of hosting offline LLMs and discuss some strategies for
|
||||
hosting them safely with Podman, Bifrost, and Caddy. We will also discuss the
|
||||
current state of LLM hardware and give some realistic examples with AMD, Intel,
|
||||
Nvidia, and CPU based solutions. We will not be using cloud examples, as this
|
||||
talk will focus on avoiding cloud solutions in general. We will poke fun at
|
||||
leveraging Discord as our example if our goal is to self-host.
|
||||
|
||||
Ultimately, I want this talk's participants to leave with some functional code
|
||||
and good ideas to get them thinking about ways they can integrate LLMs into
|
||||
their communities while maintaining control and privacy (and avoiding a hefty
|
||||
bill). This talk will emphasize audience participation to generate ideas for a
|
||||
prebuilt demo of the custom bot service, but will not build anything live
|
||||
during the presentation.
|
||||
109
presentations/self_hosting_presentation.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Idea
|
||||
|
||||
## Abstract
|
||||
|
||||
Ever find yourself Googling "how do I build a home server" only to get
|
||||
overwhelmed by enterprise-grade documentation? Welcome to our journey from
|
||||
confused beginner to building a home server that's actually useful.
|
||||
|
||||
Join us as we walk through real-world projects that solved actual problems:
|
||||
backing up family photos, hosting private Git repositories, running local AI
|
||||
models, managing home media, and yes—even running multiple Minecraft servers on
|
||||
a single box. We'll explore hardware choices, operating systems,
|
||||
containerization vs VMs, and the pain points that motivated each decision.
|
||||
|
||||
This isn't a theory-heavy presentation—it's a story-driven exploration of
|
||||
building infrastructure for real people with real needs. When you're done,
|
||||
you'll leave with a roadmap for your own server that balances automation,
|
||||
redundancy, and the "I just want this to work" factor.
|
||||
|
||||
Some prior sysadmin knowledge required. All projects are from personal
|
||||
experience, with stories about what went wrong and how we fixed it.
|
||||
|
||||
## Structure
|
||||
|
||||
"I'm lazy, I don't want my family to kill me, and it works"
|
||||
|
||||
1. I want this in my house
|
||||
2. I want to connect outside my house
|
||||
3. I want my friends to connect
|
||||
4. I don't want this to go down
|
||||
5. I want to recover if there's an error
|
||||
6. My house burned down, what now?
|
||||
|
||||
## Thoughts
|
||||
|
||||
Give 2 ideas per section. First for "I can't let this break my family will kill
|
||||
me". Second for "I have an understanding partner who is my cat and won't care."
|
||||
|
||||
Story driven presentation
|
||||
|
||||
I have decided to make a strong home server. Where do I even start?
|
||||
|
||||
Hardware: you find a box (old laptop, rpi) you're set.
|
||||
|
||||
- Operating system (proxmox, truenas, fedora, arch linux)
|
||||
- Alex: truenas apps
|
||||
- Reese: Fedora, osbuild
|
||||
|
||||
1. Install native app (npm, pip, apt, dnf, etc)
|
||||
2. Containerized (kube, docker, podman)
|
||||
3. VM (vm, pick one or two)
|
||||
|
||||
- Ingress (nginx, caddy, haproxy)
|
||||
- Backups (rsync, borg, btrfs send, zfs send)
|
||||
|
||||
1. I want to install a new app while I'm at friend's house
|
||||
1. Truenas web portal (app page, both official and community)
|
||||
2. VPN and I need access to my computer
|
||||
2. I want to check my server status on my phone (updates, disks, memory pressure, error logs, services running)
|
||||
1. Truenas web interface
|
||||
2. Cockpit web interface
|
||||
3. I want to add more storage
|
||||
1. Truenas ZFS storage pools
|
||||
2. BTRFS pools
|
||||
4. I want to install a new alpha app without much support
|
||||
1. Truenas custom docker compose images
|
||||
2. Fedora clone and run (in a VM for style)
|
||||
5. I want to backup my photos
|
||||
1. Google Photos: don't use git, images aren't meant for git
|
||||
2. **Immich, with backups (tell stories about losing my image data)**
|
||||
6. I want a local copy of my code
|
||||
1. Github
|
||||
2. Gitea/Gitlab (talk about that transition)
|
||||
7. I want private document editing
|
||||
1. Google drive, Obsidian (forces use of markdown as my standard)
|
||||
2. VSCode + pandoc (commit markdown files as your documents)
|
||||
3. Nextcloud (Collabora)
|
||||
8. I want a local, offline LLMs
|
||||
1. llama.cpp, stable diffusion cpp, bifrost
|
||||
2. Ollama is switching to cloud based models
|
||||
9. I want to watch media I own
|
||||
1. Plex boi - I know that ruffles some jimmies. Give example: add letterbox support into Plex.
|
||||
2. Jellyfin if you're cheap
|
||||
10. I want to know when something goes wrong
|
||||
1. Uptime Kuma!
|
||||
2. Truenas sending emails if there's an error
|
||||
3. Fedora requires a custom solution.
|
||||
11. I want "reasonable availability"
|
||||
1. Truenas hits 90%+ availability. Updates take it down for reboot (5-10
|
||||
minutes). Disk failure requires full shutdown, disk swap, and rebuild.
|
||||
This could be half a day.
|
||||
2. Fedora hits 90%+ availability. Updates take it down for reboot (<1 min).
|
||||
Disk failures can be ignored by rebalancing. Disk failures still require
|
||||
full shutdown and resilver. This can take half a day.
|
||||
12. I want to host multiple minecraft servers (SRV records)
|
||||
1. AWS Route53 for automating SRV records.
|
||||
2. Pihole is in the territory of making your family mad
|
||||
13. I want to automate my house
|
||||
1. Home Assistant (raspberry pi or green)
|
||||
14. I want backups of all my data
|
||||
1. No backups is an option
|
||||
2. Local weekly backups to usb drives via Truenas data replication
|
||||
3. Borg backup via CLI or Pika.
|
||||
4. Full disk backups, app directory backups, hybrid model
|
||||
5. Backblaze and S3 integration for Truenas
|
||||
6. 3 copies of your data, 2 different media,1 off site.
|
||||
15. I want a private VPN
|
||||
1. Tailscale, moved from wifiman, also moved from pivpn
|
||||
2. unifi wireguard server, rawdog wireguard on a pi
|
||||
@@ -5,8 +5,10 @@ requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"click==8.2.1",
|
||||
"dotenv>=0.9.9",
|
||||
"fastapi>=0.135.3",
|
||||
"langchain>=1.2.13",
|
||||
"langchain-openai>=1.1.12",
|
||||
"mcp>=1.27.0",
|
||||
"mkdocs>=1.6.1",
|
||||
"openai>=2.21.0",
|
||||
"pika>=1.3.2",
|
||||
@@ -14,7 +16,14 @@ dependencies = [
|
||||
"pytest>=9.0.2",
|
||||
"pyyaml>=6.0.3",
|
||||
"requests>=2.32.5",
|
||||
"sse-starlette>=3.3.4",
|
||||
"tqdm>=4.67.3",
|
||||
"types-pyyaml>=6.0.12.20250915",
|
||||
"types-tqdm>=4.67.3.20260205",
|
||||
"uvicorn>=0.44.0",
|
||||
]
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = [
|
||||
"active/vibe_mcp",
|
||||
]
|
||||
|
||||
22
stories/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Stories
|
||||
|
||||
Stories I want to tell. Unlike the `active` project, which stores notes in no
|
||||
particular order, stories are meant to be read and enjoyed from top to bottom.
|
||||
Hopefully they teach you something.
|
||||
|
||||
## This is a mkdocs project
|
||||
|
||||
`docs/` contains all the stories.
|
||||
|
||||
`mkdocs.yml` holds the project config.
|
||||
|
||||
```bash
|
||||
# Run the mkdocs site with mkdocs serve
|
||||
uv run mkdocs serve
|
||||
```
|
||||
|
||||
## Errata
|
||||
|
||||
mkdocs has a [bug that breaks
|
||||
autoreload](https://github.com/mkdocs/mkdocs/issues/4032). This project has
|
||||
pinned click to `8.2.1` to fix it.
|
||||
92
stories/docs/10-fedora_server_admin.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# I want to build the perfect homelab server
|
||||
|
||||
Date Written: 02/11/26
|
||||
|
||||
Fedora Version: 43
|
||||
|
||||
## Intro
|
||||
|
||||
And it will run Fedora. Backstory: I ran Truenas for a long time. I started with
|
||||
Freenas, got confused by jails, switched to this new thing called Truenas,
|
||||
learned about ZFS, watched "Apps" come into existence, watched Kubernetes
|
||||
support rise and fall, watched the disaster that was Incus container/VM support
|
||||
and the subsequent "rollback" to traditional VMs, and got really tired of lack
|
||||
of control.
|
||||
|
||||
App backup and restore was never, and still isn't, well supported. Taking
|
||||
snapshots of your apps pool and then sending those snapshots to a backup drive
|
||||
un-hid the ix-systems directory (which would frequently have thousands of
|
||||
snapshots due to Truenas's liberal use of subvolumes and would slow down the UI
|
||||
immensely). App data was intentionally hidden from the user for some reason.
|
||||
Migrating between Docker, then kubernetes, then Incus was never fully planned.
|
||||
The Truenas Charts app market was awesome, but building a Truenas Chart was
|
||||
complex and required duplicating all `values.yaml` configurable parameters into
|
||||
a new yaml file that the UI could use for form-fill. They got rid of Truenas
|
||||
Charts regardless so you just have to hope that your favorite app supporter is
|
||||
comfortable rewriting their app in the new format and supporting some kind of
|
||||
migration strategy. Every six months I would expect some kind of downtime
|
||||
because Truenas would change something critical and it would inevitably impact
|
||||
my workflow.
|
||||
|
||||
So, if I'm going to be subject to the whims of a changing platform anyway (given
|
||||
Truenas is supposed to be based on Debian, aka the *stable* choice), and if I'm
|
||||
going to suffer breaking changes every 6 months no matter what I choose, then I
|
||||
may as well have the latest and greatest via a rolling kernel distro.
|
||||
|
||||
So why not Arch? Simply: SELinux. SELinux is currently not officially supported
|
||||
in Arch linux. Plus Fedora Server comes with a lot built in that I like.
|
||||
Cockpit, Firewalld, Podman, SELinux, OSBuild, and RPM support all work out of
|
||||
the box. These are, imo, the "bare bones" requirements for a server exposed to
|
||||
the internet that will run homelab services.
|
||||
|
||||
So let's get started configuring an awesome Fedora server to keep your data safe
|
||||
and run your Homelab services with minimal downtime.
|
||||
|
||||
## Installation
|
||||
|
||||
When installing Fedora from the ISO, take some time at the installation menu to
|
||||
configure some basics.
|
||||
|
||||
Don't worry about RAID for now, we can convert a single disk into a RAID 1 array
|
||||
later.
|
||||
|
||||
If you don't have an SSH key already, generate one for yourself so you can log into the server. On your local machine:
|
||||
|
||||
```bash
|
||||
# Generate the key
|
||||
# Save it to the default location (~/.ssh/id_ed25519)
|
||||
# Please please please encrypt it with a password. Something memorable. Write it down. Friends don't let friends have naked SSH keys.
|
||||
ssh-keygen -t ed25519
|
||||
```
|
||||
|
||||
1. Configure the network
|
||||
1. Set a hostname
|
||||
2. Disable ipv6 privacy extensions
|
||||
2. Configure software selection
|
||||
1. Choose anything you'd like preinstalled
|
||||
3. Create a non-root user
|
||||
1. Set a simple password for easy login, we'll change it later
|
||||
4. Configure your disk partitioning
|
||||
1. Select manual (blivet) partitioning
|
||||
2. Create a 1GB EFI system partition and mount it at `/boot/efi`
|
||||
3. Create a 1GB btrfs partition and mount it at `/boot`
|
||||
4. Create an encrypted btrfs volume with the remaining data and name it something unique, do not mount it
|
||||
5. Create a btrfs subvolume called "root" and mount it at `/`
|
||||
6. Create a btrfs subvolume called "home" and mount it at `/home`
|
||||
7. Create any other btrfs subvolumes you might need (`/var`, for example)
|
||||
5. Take note of the ipv4 and ipv6 address. Update any DNS records at this time.
|
||||
6. Install and reboot
|
||||
|
||||
## Configuration
|
||||
|
||||
Once your server boots up we'll follow a basic playbook:
|
||||
|
||||
1. Change your password
|
||||
2. Configure automatic decryption for your encrypted drives at boot with TPM2
|
||||
3. Configure the package manager and apply updates
|
||||
4. Secure SSH with Fail2Ban
|
||||
5. Install Snapper for automatic snapshots to prevent accidental file deletion
|
||||
6. Install BorgBackup for automatic backups
|
||||
7. Install VM support
|
||||
8. Build some images
|
||||
9. Run some VMs
|
||||
224
stories/docs/20-local_llms.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# I refuse to pay for LLMs
|
||||
|
||||
But I want them anyway. And I don't just want LLMs, I want:
|
||||
|
||||
1. Image Generation
|
||||
2. Image Editing
|
||||
3. Speech to Text
|
||||
4. Text to Speech
|
||||
5. Web Searching
|
||||
6. RAG Retrieval
|
||||
7. Guest accounts with time-based access
|
||||
8. Probably other things
|
||||
|
||||
On rootless podman with snapshots and backups and no compromises.
|
||||
|
||||
- [I refuse to pay for LLMs](#i-refuse-to-pay-for-llms)
|
||||
- [Create your environment](#create-your-environment)
|
||||
- [Local LLM First](#local-llm-first)
|
||||
- [Ollama](#ollama)
|
||||
- [LM Studio](#lm-studio)
|
||||
- [llama.cpp](#llamacpp)
|
||||
- [Ok, so you have a backend](#ok-so-you-have-a-backend)
|
||||
- [What about llama-server?](#what-about-llama-server)
|
||||
- [Anything LLM](#anything-llm)
|
||||
- [Open Webui](#open-webui)
|
||||
- [But we don't have image editing working](#but-we-dont-have-image-editing-working)
|
||||
- [Stable Diffusion CPP](#stable-diffusion-cpp)
|
||||
- [Making it Run with Quadlets](#making-it-run-with-quadlets)
|
||||
|
||||
## Create your environment
|
||||
|
||||
I created a user named `ai` to run all my AI services. Do that now:
|
||||
|
||||
```bash
|
||||
useradd -m ai
|
||||
loginctl enable-linger ai
|
||||
su -l ai
|
||||
mkdir -p /home/ai/.config/containers/systemd/
|
||||
mkdir -p /home/ai/.ssh
|
||||
```
|
||||
|
||||
## Local LLM First
|
||||
|
||||
On the Framework Desktop (or any AMD system) your options are ROCM or Vulkan drivers. Both are fine, with Vulkan pulling slightly ahead as of February 2026. Almost every backend you pick will support both, so pick a backend first.
|
||||
|
||||
### Ollama
|
||||
|
||||
is the natural place to start. Their "marketplace" is the best I've found for browsing models. They include short descriptions about what the models are good for and (almost) all of them work out of the box!
|
||||
|
||||
Bonus points: Ollama's API is well supported by interfaces like Anything LLM, Open Webui, a litany of F-Droid apps, and many other services.
|
||||
|
||||
Honestly, Ollama is still where I'd recommend anyone start. The installer is easy, performance is decent, the API is great, they (the Ollama team) curate models that work well on their platform, what's not to like?
|
||||
|
||||
Performance, mostly. llama.cpp just performs 20-30% better in my testing on models like gpt-oss-120b. Your mileage may vary, this is a great project.
|
||||
|
||||
### LM Studio
|
||||
|
||||
Everyone says to start with this. Ok, first of all, it's a GUI app. Yeah there's a toggle to run an API server but ain't no way I'm installing wayland on my pure, uncompromising, headless Fedora server.
|
||||
|
||||
I do have to admit it's the fastest way to get started with LLMs on desktop. But we're not here for desktops, we're here for servers. It runs llama.cpp in the backend anyway so skip past this and go for the good stuff.
|
||||
|
||||
### llama.cpp
|
||||
|
||||
We've landed on the best choice. You'll browse Hugging Face for models, be confused, and like it. You'll struggle to read the logs and feel right at home. You'll wonder why there isn't an intuitive CLI like Ollama. And you'll be rewarded with the fastest, most flexible way to run LLMs.
|
||||
|
||||
You'll need the Hugging Face CLI (`hf`). Install that.
|
||||
|
||||
First, download qwen3-vl-8b. This is a good jack of all trades model that supports vision, which is nice.
|
||||
|
||||
```bash
|
||||
# Create a directory to hold your text models
|
||||
# I put mine at /home/ai/models/text
|
||||
mkdir -p /home/ai/models/text/qwen3-vl-8b-instruct
|
||||
|
||||
# Download the model from hugging face
|
||||
hf download --local-dir /home/ai/models/text/qwen3-vl-8b-instruct Qwen/Qwen3-VL-8B-Instruct-GGUF Qwen3VL-8B-Instruct-Q4_K_M.gguf
|
||||
# Also download the "mmproj" file for this model
|
||||
# "mmproj" files allow a model to see images
|
||||
hf download --local-dir /home/ai/models/text/qwen3-vl-8b-instruct Qwen/Qwen3-VL-8B-Instruct-GGUF mmproj-Qwen3VL-8B-Instruct-Q8_0.gguf
|
||||
```
|
||||
|
||||
With our model locked and loaded, we can run the llama.cpp server. We do have to build the llama.cpp server container first though because making this any easier would be a crime.
|
||||
|
||||
```bash
|
||||
# Build the llama.cpp container image
|
||||
git clone https://github.com/ggml-org/llama.cpp.git
|
||||
cd llama.cpp
|
||||
export BUILD_TAG=$(date +"%Y-%m-%d-%H-%M-%S")
|
||||
|
||||
# Vulkan
|
||||
podman build -f .devops/vulkan.Dockerfile -t llama-cpp-vulkan:${BUILD_TAG} -t llama-cpp-vulkan:latest .
|
||||
|
||||
# Run llama server (Available on port 8000)
|
||||
# Add `--n-cpu-moe 32` to gpt-oss-120b to keep minimal number of expert in GPU
|
||||
podman run \
|
||||
--rm \
|
||||
--name llama-server-demo \
|
||||
--device=/dev/kfd \
|
||||
--device=/dev/dri \
|
||||
--pod systemd-ai-internal \
|
||||
-v /home/ai/models/text:/models:z \
|
||||
localhost/llama-cpp-vulkan:latest \
|
||||
--port 8000 \
|
||||
-c 16384 \
|
||||
--perf \
|
||||
--n-gpu-layers all \
|
||||
--jinja \
|
||||
--models-max 1 \
|
||||
--models-dir /models
|
||||
```
|
||||
|
||||
You should be able to access the llama.cpp server at http://{your-ip}:8000. From there you can select the only model you have downloaded (qwen3-vl-8b) and have a conversation.
|
||||
|
||||
## Ok, so you have a backend
|
||||
|
||||
Now we need a frontend. In my experience there are only 2 choices, but this is changing extremely fast.
|
||||
|
||||
### What about llama-server?
|
||||
|
||||
Good enough for testing. Honestly, if this meets your needs, more power to you.
|
||||
|
||||
### Anything LLM
|
||||
|
||||
I started here about a year ago. This is a fantastic frontend with RAG, speech to text, text to speech, web search, RAG, plugins, and decent user management. It supports Ollama, OpenAI, and a bunch of other backends.
|
||||
|
||||
Unfortunately, as of when I used it, there was no integrated image generation or image editing.
|
||||
|
||||
### Open Webui
|
||||
|
||||
This is, in my opinion, the best frontend experience you can get. The killer feature is side-by-side HTML rendering with your LLM response. If your LLM writes HTML/Javascript/CSS, it'll render in real time next to your chat. That's ridiculously cool.
|
||||
|
||||
It also supports image generation as a tool that your LLM can call. Prompts like "Generate an image of a dragon" will trigger a call to the image generation tool. Generated images show up in the chat and can be edited with another message.
|
||||
|
||||
```bash
|
||||
mkdir /home/ai/.env
|
||||
vim /home/ai/.env/open-webui-env
|
||||
|
||||
# Add this to the file, then save an exit
|
||||
WEBUI_SECRET_KEY="some-random-key"
|
||||
|
||||
# Will be available on port 8080
|
||||
podman run \
|
||||
-d \
|
||||
-p 8080 \
|
||||
-v open-webui:/app/backend/data \
|
||||
--env-file /home/ai/.env/open-webui-env \
|
||||
--name open-webui \
|
||||
--restart always \
|
||||
ghcr.io/open-webui/open-webui:main
|
||||
```
|
||||
|
||||
Use the following connections when configuring models/image editing:
|
||||
|
||||
| Service | Endpoint |
|
||||
| -------------------- | ----------------------------------------- |
|
||||
| llama.cpp | <http://host.containers.internal:8000> |
|
||||
| stable-diffusion.cpp | <http://host.containers.internal:1234/v1> |
|
||||
|
||||
## But we don't have image editing working
|
||||
|
||||
In the past I used stable-diffusion-webui-forge. This project relied on a very
|
||||
specific set of ROCM torch versions installed via pip from the nightly ROCM pip
|
||||
repository. I had Stable Diffusion XL and Flux1.dev working on an AMD GPU, but I
|
||||
couldn't get this working at all on the Framework Desktop.
|
||||
|
||||
I found out later this might be due to a ROCM driver bug, but we have bigger and better projects to work with.
|
||||
|
||||
### Stable Diffusion CPP
|
||||
|
||||
This project is llama.cpp equivalent for image generation. Open AI compatible API, tons of model support, excellent documentation, it's the best.
|
||||
|
||||
```bash
|
||||
# Clone and build the stable diffusion cpp container
|
||||
git clone https://github.com/leejet/stable-diffusion.cpp.git
|
||||
cd stable-diffusion.cpp
|
||||
git submodule update --init --recursive
|
||||
export BUILD_TAG=$(date +"%Y-%m-%d-%H-%M-%S")
|
||||
podman build -f Dockerfile.vulkan -t stable-diffusion-cpp:${BUILD_TAG} -t stable-diffusion-cpp:latest .
|
||||
```
|
||||
|
||||
Stable diffusion CPP supports a CLI and a web server. Let's download a model and test out the CLI.
|
||||
|
||||
```bash
|
||||
# z-turbo image model
|
||||
# Fastest image generation in 8 steps. Great a text and prompt following.
|
||||
# Lacks variety.
|
||||
mkdir -p /home/ai/models/image/z-turbo
|
||||
hf download --local-dir /home/ai/models/image/z-turbo QuantStack/FLUX.1-Kontext-dev-GGUF flux1-kontext-dev-Q4_K_M.gguf
|
||||
hf download --local-dir /home/ai/models/image/z-turbo black-forest-labs/FLUX.1-schnell ae.safetensors
|
||||
hf download --local-dir /home/ai/models/image/z-turbo unsloth/Qwen3-4B-Instruct-2507-GGUF Qwen3-4B-Instruct-2507-Q4_K_M.gguf
|
||||
|
||||
# Create our output directory
|
||||
mkdir /home/ai/output
|
||||
|
||||
# Generate an image of a photorealistic dragon.
|
||||
podman run --rm \
|
||||
-v /home/ai/models:/models:z \
|
||||
-v /home/ai/output:/output:z \
|
||||
--device /dev/kfd \
|
||||
--device /dev/dri \
|
||||
localhost/stable-diffusion-cpp:latest \
|
||||
--diffusion-model /models/image/z-turbo/z_image_turbo-Q4_K.gguf \
|
||||
--vae /models/image/z-turbo/ae.safetensors \
|
||||
--llm /models/image/z-turbo/Qwen3-4B-Instruct-2507-Q4_K_M.gguf \
|
||||
--cfg-scale 1.0 \
|
||||
-v \
|
||||
--seed -1 \
|
||||
--steps 8 \
|
||||
--vae-conv-direct \
|
||||
-H 1024 \
|
||||
-W 1024 \
|
||||
-o /output/output.png \
|
||||
-p "A photorealistic dragon"
|
||||
```
|
||||
|
||||
With any luck you should have a picture of a dragon in your output folder.
|
||||
|
||||
Since we know it works, we can tie everything together.
|
||||
|
||||
## Making it Run with Quadlets
|
||||
|
||||
Now that we have know our setup works we can glue it all together with systemd.
|
||||
|
||||
Take a look at [the framework desktop docs](https://gitea.reeseapps.com/services/homelab/src/branch/main/active/device_framework_desktop/framework_desktop.md#install-the-whole-thing-with-quadlets-tm) for the relevant commands.
|
||||
1
stories/docs/30-gpg_signing.md
Normal file
@@ -0,0 +1 @@
|
||||
# Everyone uses this GPG thing, so should I
|
||||
1
stories/docs/40-podman_rootless_hosting.md
Normal file
@@ -0,0 +1 @@
|
||||
# I want to use Podman, not Docker
|
||||
11
stories/docs/index.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Come, have a seat
|
||||
|
||||
Join me on a journey through homelab adventures. Follow along at home! These
|
||||
stores will walk you through the trials of my self hosting wins and losses.
|
||||
|
||||
The stories will be written in a way that allows you to skip past the text and
|
||||
just copy/paste the code blocks (similar to a Medium article). Each story will
|
||||
lay out its goal and the prerequisites.
|
||||
|
||||
Stories are ordered by time written, oldest to newest. They don't necessarily
|
||||
read in order, but may reference each other. No need to read each one.
|
||||
3
stories/mkdocs.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
site_name: Reese's Homelab Stories
|
||||
theme:
|
||||
name: readthedocs
|
||||
377
uv.lock
generated
@@ -2,6 +2,15 @@ version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
version = "0.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
@@ -23,6 +32,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "26.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.1.4"
|
||||
@@ -32,6 +50,51 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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 = [
|
||||
{ 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]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.4"
|
||||
@@ -94,6 +157,59 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "46.0.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
@@ -114,6 +230,22 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.135.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f7/e6/7adb4c5fa231e82c35b8f5741a9f2d055f520c29af5546fd70d3e8e1cd2e/fastapi-0.135.3.tar.gz", hash = "sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654", size = 396524, upload-time = "2026-04-01T16:23:58.188Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/a4/5caa2de7f917a04ada20018eccf60d6cc6145b0199d55ca3711b0fc08312/fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98", size = 117734, upload-time = "2026-04-01T16:23:59.328Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghp-import"
|
||||
version = "2.1.0"
|
||||
@@ -142,8 +274,10 @@ source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "dotenv" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "langchain" },
|
||||
{ name = "langchain-openai" },
|
||||
{ name = "mcp" },
|
||||
{ name = "mkdocs" },
|
||||
{ name = "openai" },
|
||||
{ name = "pika" },
|
||||
@@ -151,17 +285,21 @@ dependencies = [
|
||||
{ name = "pytest" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "requests" },
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "types-pyyaml" },
|
||||
{ name = "types-tqdm" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "click", specifier = "==8.2.1" },
|
||||
{ name = "dotenv", specifier = ">=0.9.9" },
|
||||
{ name = "fastapi", specifier = ">=0.135.3" },
|
||||
{ name = "langchain", specifier = ">=1.2.13" },
|
||||
{ name = "langchain-openai", specifier = ">=1.1.12" },
|
||||
{ name = "mcp", specifier = ">=1.27.0" },
|
||||
{ name = "mkdocs", specifier = ">=1.6.1" },
|
||||
{ name = "openai", specifier = ">=2.21.0" },
|
||||
{ name = "pika", specifier = ">=1.3.2" },
|
||||
@@ -169,9 +307,11 @@ requires-dist = [
|
||||
{ name = "pytest", specifier = ">=9.0.2" },
|
||||
{ name = "pyyaml", specifier = ">=6.0.3" },
|
||||
{ name = "requests", specifier = ">=2.32.5" },
|
||||
{ name = "sse-starlette", specifier = ">=3.3.4" },
|
||||
{ name = "tqdm", specifier = ">=4.67.3" },
|
||||
{ name = "types-pyyaml", specifier = ">=6.0.12.20250915" },
|
||||
{ name = "types-tqdm", specifier = ">=4.67.3.20260205" },
|
||||
{ name = "uvicorn", specifier = ">=0.44.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -202,6 +342,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
version = "0.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
@@ -304,6 +453,33 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "4.26.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "jsonschema-specifications" },
|
||||
{ name = "referencing" },
|
||||
{ name = "rpds-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema-specifications"
|
||||
version = "2025.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "referencing" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langchain"
|
||||
version = "1.2.13"
|
||||
@@ -488,6 +664,31 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.27.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx-sse" },
|
||||
{ name = "jsonschema" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "pyjwt", extra = ["crypto"] },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mergedeep"
|
||||
version = "1.3.4"
|
||||
@@ -725,6 +926,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.12.5"
|
||||
@@ -793,6 +1003,20 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.13.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
@@ -802,6 +1026,20 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
crypto = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.2"
|
||||
@@ -839,6 +1077,28 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.24"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/45/e23b5dc14ddb9918ae4a625379506b17b6f8fc56ca1d82db62462f59aea6/python_multipart-0.0.24.tar.gz", hash = "sha256:9574c97e1c026e00bc30340ef7c7d76739512ab4dfd428fec8c330fa6a5cc3c8", size = 37695, upload-time = "2026-04-05T20:49:13.829Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/73/89930efabd4da63cea44a3f438aeb753d600123570e6d6264e763617a9ce/python_multipart-0.0.24-py3-none-any.whl", hash = "sha256:9b110a98db707df01a53c194f0af075e736a770dc5058089650d70b4a182f950", size = 24420, upload-time = "2026-04-05T20:49:12.555Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32"
|
||||
version = "311"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
@@ -887,6 +1147,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "referencing"
|
||||
version = "0.37.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "rpds-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "2026.2.28"
|
||||
@@ -986,6 +1259,72 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rpds-py"
|
||||
version = "0.30.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
@@ -1004,6 +1343,31 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "3.3.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "starlette" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427, upload-time = "2026-03-29T09:00:23.307Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330, upload-time = "2026-03-29T09:00:21.846Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tenacity"
|
||||
version = "9.1.4"
|
||||
@@ -1150,6 +1514,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.44.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "watchdog"
|
||||
version = "6.0.0"
|
||||
|
||||