import all
This commit is contained in:
commit
881baaa1ed
25 files changed
+1779
No files matched your search
@@ -0,0 +1,120 @@
|
||||
import aiohttp
|
||||
import json
|
||||
import dotenv
|
||||
import os
|
||||
import importlib.metadata
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
api_key = os.getenv("BRICKOWL")
|
||||
abrickowl_version = importlib.metadata.version("abrickowlapi")
|
||||
|
||||
|
||||
class ABrickowlAPI:
|
||||
|
||||
def __init__(self, value):
|
||||
self.user_agent = f"abrickowlapi/{abrickowl_version}"
|
||||
self.headers = {"content-type": "application/x-www-form-urlencoded"}
|
||||
self.base_url = "https://api.brickowl.com/v1/"
|
||||
self.session = aiohttp.ClientSession()
|
||||
return self
|
||||
|
||||
def set_valid(self, set_id: int):
|
||||
"""
|
||||
Verify if a LEGO set ID is valid.
|
||||
"""
|
||||
if len(set_id) == 5:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def catalog_list(self):
|
||||
"""
|
||||
Fetch the list of LEGO sets from the Brickowl API.
|
||||
"""
|
||||
async with self.session.get(
|
||||
f"{self.base_url}catalog/list/view?key={api_key}",
|
||||
headers=self.headers,
|
||||
user_agent=self.user_agent,
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
return await response.json()
|
||||
else:
|
||||
raise Exception(f"Error fetching list, {response.status}")
|
||||
|
||||
@classmethod
|
||||
async def catalog_lookup(self, set_id: int):
|
||||
"""
|
||||
Fetch details from the brickowl API for a specific LEGO set.
|
||||
|
||||
set_id: The LEGO set ID to look up.
|
||||
|
||||
returns JSON response containing set details.
|
||||
"""
|
||||
async with self.session.get(
|
||||
f"{self.base_url}catalog/lookup/{set_id}view?key={api_key}",
|
||||
headers=self.headers,
|
||||
user_agent=self.user_agent,
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
return await response.json()
|
||||
else:
|
||||
raise Exception(f"Error fetching set {set_id}, {response.status}")
|
||||
|
||||
@classmethod
|
||||
async def catalog_availability(self, set_id: int):
|
||||
"""
|
||||
Fetch availability information for a specific LEGO set.
|
||||
"""
|
||||
async with self.session.get(
|
||||
f"{self.base_url}catalog/availability/{set_id}view?key={api_key}",
|
||||
headers=self.headers,
|
||||
user_agent=self.user_agent,
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
return await response.json()
|
||||
else:
|
||||
raise Exception(
|
||||
f"Error fetching fetching availability for {set_id}, {response.status}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def catalog_id_lookup(self, set_id: int):
|
||||
async with self.session.get(
|
||||
f"{self.base_url}catalog/id_lookup/{set_id}view?key={api_key}",
|
||||
headers=self.headers,
|
||||
user_agent=self.user_agent,
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
return await response.json()
|
||||
else:
|
||||
raise Exception(f"ID lookup failed for set {set_id}, {response.status}")
|
||||
|
||||
@classmethod
|
||||
async def catalog_bulk(self):
|
||||
async with self.session.get(
|
||||
f"{self.base_url}catalog/bulk/view?key={api_key}",
|
||||
headers=self.headers,
|
||||
user_agent=self.user_agent,
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
return await response.json()
|
||||
else:
|
||||
raise Exception(f"Error fetching bulk, {response.status}")
|
||||
|
||||
@classmethod
|
||||
async def catalog_search(self, query: str):
|
||||
async with self.session.get(
|
||||
f"{self.base_url}catalog/search/{query}view?key={api_key}",
|
||||
headers=self.headers,
|
||||
user_agent=self.user_agent,
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
return await response.json()
|
||||
else:
|
||||
raise Exception(f"Search term {set_id} failed, {response.status}")
|
||||
|
||||
|
||||
# Usage:
|
||||
# obj = await ABrickowlAPI.catalog_lookup(value)
|
||||
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
abricksetapi.py - Asynchronous Brickset API Client
|
||||
This module provides asynchronous interactions with the Brickset API.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import aiohttp
|
||||
import json
|
||||
import dotenv
|
||||
from typing import Dict, Any
|
||||
from asyncio_throttle.throttler import Throttler
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
|
||||
class AsyncBricksetAPI:
|
||||
"""Asynchronous client for the Brickset API"""
|
||||
|
||||
def __init__(self, debug: bool = False):
|
||||
self.username = os.getenv("BRICKSET_USERNAME")
|
||||
self.password = os.getenv("BRICKSET_PASS")
|
||||
self.api_key = os.getenv("BRICKSET_API_KEY")
|
||||
self.debug = debug
|
||||
self.headers = {"UserAgent": "Edderkop/0.1"}
|
||||
self.throttler = Throttler(rate_limit=500, period=60)
|
||||
self.user_hash = None
|
||||
|
||||
async def login(self) -> Dict[str, Any]:
|
||||
"""Asynchronously authenticate with Brickset API"""
|
||||
url = "https://brickset.com/api/v3.asmx/login"
|
||||
params = {
|
||||
"username": self.username,
|
||||
"password": self.password,
|
||||
"apiKey": self.api_key,
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
url, params=params, headers=self.headers # type: ignore
|
||||
) as response:
|
||||
if self.debug:
|
||||
print(f"[@DEBUG] Login response status: {response.status}")
|
||||
|
||||
data = await response.read()
|
||||
result = json.loads(data.decode("utf-8"))
|
||||
self.user_hash = result.get("hash")
|
||||
return result
|
||||
|
||||
async def get_set(self, set_number: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Fetch LEGO set information asynchronously
|
||||
|
||||
Args:
|
||||
set_number (str): LEGO set number
|
||||
|
||||
Returns:
|
||||
Dict containing set information
|
||||
"""
|
||||
if not self.user_hash:
|
||||
await self.login()
|
||||
|
||||
url = "https://brickset.com/api/v3.asmx/getSets"
|
||||
params = {
|
||||
"apiKey": self.api_key,
|
||||
"userHash": self.user_hash,
|
||||
"params": json.dumps({"setNumber": f"{set_number}-1"}),
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
url, params=params, headers=self.headers
|
||||
) as response:
|
||||
if self.debug:
|
||||
print(f"[@DEBUG] Get set response status: {response.status}")
|
||||
|
||||
if response.status == 200:
|
||||
data = await response.read()
|
||||
result = json.loads(data.decode("utf-8"))
|
||||
|
||||
if not result.get("sets"):
|
||||
raise ValueError(f"No data found for set {set_number}")
|
||||
|
||||
set_data = result["sets"][0]
|
||||
our_set = {
|
||||
"set_num": set_data["number"],
|
||||
"set_name": set_data["name"],
|
||||
"set_theme": set_data["theme"],
|
||||
"set_image": set_data["image"]["thumbnailURL"],
|
||||
"set_barcode": set_data["barcode"]["UPC"],
|
||||
"set_retailprice": round(
|
||||
set_data["LEGOCom"]["US"]["retailPrice"]
|
||||
),
|
||||
}
|
||||
|
||||
# Calculate adjusted price (10% markup)
|
||||
retail_price = our_set["set_retailprice"]
|
||||
our_set["set_adjustredprice"] = retail_price + (
|
||||
(10 / 100) * retail_price
|
||||
)
|
||||
|
||||
# Set PW inventory name with theme
|
||||
our_set["pw_name"] = (
|
||||
f"{our_set['set_num']} - {our_set['set_theme'].upper()} {our_set['set_name']}"
|
||||
)
|
||||
|
||||
# Flag for manual price review if needed
|
||||
if our_set["set_retailprice"] >= 110:
|
||||
our_set["requires_manual_price"] = True
|
||||
|
||||
return our_set
|
||||
|
||||
elif response.status == 500:
|
||||
raise Exception(
|
||||
"Internal Server Error: Please check API key and parameters."
|
||||
)
|
||||
else:
|
||||
raise Exception(f"Unexpected response: {response.status}")
|
||||
|
||||
async def throttled_get_set(self, set_number: str) -> Dict[str, Any]:
|
||||
"""Get set information with rate limiting"""
|
||||
async with self.throttler:
|
||||
return await self.get_set(set_number)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Example usage"""
|
||||
client = AsyncBricksetAPI(debug=True)
|
||||
try:
|
||||
set_info = await client.throttled_get_set("10349")
|
||||
print(json.dumps(set_info, indent=2))
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
import os
|
||||
import dotenv
|
||||
import json
|
||||
import asyncio
|
||||
import uuid
|
||||
import re
|
||||
|
||||
import get_sq_env
|
||||
|
||||
|
||||
def get_set_number(text: str) -> int:
|
||||
# Remove all letters, keeping only digits and spaces
|
||||
numbers = re.findall(r"\d{5}", text)
|
||||
if not numbers:
|
||||
raise ValueError("No five-digit number found in string")
|
||||
return int(numbers[0])
|
||||
|
||||
|
||||
SQUARE_TOKEN = get_sq_env.SQUARE_TOKEN
|
||||
LEGO_CAT_ID = get_sq_env.LEGO_CAT_ID
|
||||
ENVIRONMENT = get_sq_env.environment
|
||||
|
||||
|
||||
from square import AsyncSquare
|
||||
from square.environment import SquareEnvironment
|
||||
|
||||
client = AsyncSquare(token=SQUARE_TOKEN, environment=ENVIRONMENT) # type: ignore
|
||||
|
||||
|
||||
async def get_itm(sku: str):
|
||||
"""
|
||||
Asynchronously retrieves item details from Square Catalog using a SKU.
|
||||
|
||||
Args:
|
||||
sku (str): The SKU or text filter to search for the item.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the item's name and ID.
|
||||
|
||||
Raises:
|
||||
IndexError: If no items are found matching the SKU.
|
||||
Exception: If there is an error communicating with the Square API.
|
||||
"""
|
||||
result = await client.catalog.search_items(text_filter=sku, limit=1)
|
||||
result_json = json.loads(result.model_dump_json())
|
||||
name = result_json["items"][0]["item_data"]["name"]
|
||||
itm_id = result_json["items"][0]["id"]
|
||||
return {"name": name, "id": itm_id}
|
||||
|
||||
|
||||
async def update_itm(lego_set: dict):
|
||||
"""
|
||||
Asynchronously updates an item in the Square catalog based on the provided LEGO set information.
|
||||
Args:
|
||||
lego_set (dict): A dictionary containing LEGO set details. Expected keys include:
|
||||
- "sets": List of set dictionaries, each with "name", "number", "barcode" (with "UPC"), and "image" (with "thumbnailURL").
|
||||
- "pw_name": The name associated with the LEGO set in another system.
|
||||
- "set_adjustredprice": The adjusted price for the set.
|
||||
Returns:
|
||||
bool: True if the update operation was successful.
|
||||
Raises:
|
||||
KeyError: If expected keys are missing from the input dictionary.
|
||||
Exception: For errors during Square API interaction.
|
||||
"""
|
||||
|
||||
set_name = lego_set["sets"][0]["name"]
|
||||
set_number = lego_set["sets"][0]["number"]
|
||||
set_upc = lego_set["sets"][0]["barcode"]["UPC"]
|
||||
set_image = lego_set["sets"][0]["image"]["thumbnailURL"]
|
||||
pw_name = lego_set["pw_name"]
|
||||
set_adjustedprice = lego_set["set_adjustredprice"]
|
||||
|
||||
result = await client.catalog.search_items(text_filter=set_number, limit=1)
|
||||
itm_id = json.loads(result.model_dump_json())["items"][0]["id"]
|
||||
|
||||
# await sq.catalog.object.upsert()
|
||||
return True
|
||||
|
||||
|
||||
async def acat2id(cat_id: str):
|
||||
"""
|
||||
Return a list of dictionaries containing item IDs and names within the specified category ID.
|
||||
|
||||
Args:
|
||||
cat_id (str): The category ID to search for items.
|
||||
|
||||
Returns:
|
||||
list[dict]: A list of dictionaries, each containing:
|
||||
- 'id': The item's ID
|
||||
- 'name': The item's name
|
||||
"""
|
||||
result = await client.catalog.search(
|
||||
object_types=["ITEM"],
|
||||
query={
|
||||
"exact_query": {
|
||||
"attribute_name": "category_id",
|
||||
"attribute_value": cat_id,
|
||||
}
|
||||
},
|
||||
)
|
||||
result_json = json.loads(result.model_dump_json())
|
||||
items = [
|
||||
{
|
||||
"id": obj["id"],
|
||||
"name": obj.get("item_data", {}).get("name", "Unknown"),
|
||||
"number": get_set_number(obj.get("item_data", {}).get("name", ""))
|
||||
}
|
||||
for obj in result_json.get("objects", [])
|
||||
]
|
||||
|
||||
return items
|
||||
|
||||
|
||||
async def test() -> None:
|
||||
task = asyncio.create_task(acat2id(LEGO_CAT_ID)) # type: ignore
|
||||
await asyncio.sleep(1) # Give the task some time to complete
|
||||
result = await task
|
||||
print("OBJECTS IN LEGO CATEGORY:")
|
||||
for item in result:
|
||||
print(f"ID: {item['id']}, Name: {item['name']}")
|
||||
# eturn
|
||||
# pass
|
||||
|
||||
|
||||
"""
|
||||
result = await sq.catalog.search_items(text_filter="71433", limit=1)
|
||||
result_json = json.loads(result.model_dump_json())
|
||||
name = result_json["items"][0]["item_data"]["name"]
|
||||
print(f"Item name: {name}")"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"@[DEBUG] ENV:\t{ENVIRONMENT}")
|
||||
print(f"@[DEBUG] CATID:\t{LEGO_CAT_ID}")
|
||||
asyncio.run(test())
|
||||
@@ -0,0 +1,20 @@
|
||||
import subprocess
|
||||
|
||||
|
||||
def generate(text, font="smblock", filter_effect=None, colors=None):
|
||||
command = ["toilet", "-f", font]
|
||||
if filter_effect:
|
||||
command.extend(["-F", filter_effect])
|
||||
if colors:
|
||||
# Toilet uses a specific format for color arguments, e.g., --gay or --metal
|
||||
# For general colors, you might need to use specific filters or direct color codes.
|
||||
# This example focuses on filters that inherently use colors.
|
||||
pass # More complex color handling would be needed for arbitrary colors
|
||||
|
||||
command.append(text)
|
||||
|
||||
try:
|
||||
result = subprocess.run(command, capture_output=True, text=True, check=True)
|
||||
return result.stdout
|
||||
except subprocess.CalledProcessError as e:
|
||||
return f"Error executing toilet: {e}\n{e.stderr}"
|
||||
@@ -0,0 +1,60 @@
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
import dotenv
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
USERNAME = os.getenv("BRICKSET_USERNAME")
|
||||
PASS = os.getenv("BRICKSET_PASS")
|
||||
BRICKSET_API_KEY = os.getenv("BRICKSET_API_KEY")
|
||||
|
||||
debug = False # Set to True to enable debug output
|
||||
|
||||
|
||||
def login(username, password):
|
||||
url = "https://brickset.com/api/v3.asmx/login"
|
||||
headers = {"UserAgent": "Edderkop/0.1"}
|
||||
params = {
|
||||
"UserAgent": "Edderkop/0.1",
|
||||
"username": username,
|
||||
"password": password,
|
||||
"apiKey": BRICKSET_API_KEY,
|
||||
}
|
||||
response = requests.get(url, params=params, headers=headers)
|
||||
if debug:
|
||||
print(f"[@DEBUG] Login response: {response.status_code} - {response.text}\n")
|
||||
return response.json()
|
||||
|
||||
|
||||
def get_set(set_number: int) -> dict:
|
||||
login_response = login(USERNAME, PASS)
|
||||
url = f"https://brickset.com/api/v3.asmx/getSets?apiKey={BRICKSET_API_KEY}&userHash={login_response["hash"]}¶ms={{'setNumber' : '{set_number}-1'}}"
|
||||
response = requests.get(url)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
our_set = {
|
||||
"set_num": data["sets"][0]["number"],
|
||||
"set_name": data["sets"][0]["name"],
|
||||
"set_theme": data["sets"][0]["theme"],
|
||||
"set_image": data["sets"][0]["image"]["thumbnailURL"],
|
||||
"set_barcode": data["sets"][0]["barcode"]["UPC"],
|
||||
"set_retailprice": round(data["sets"][0]["LEGOCom"]["US"]["retailPrice"]),
|
||||
}
|
||||
# add PW 10% b4 tax
|
||||
our_set.update(
|
||||
{"set_adjustredprice": our_set + ((10 / 100) * our_set["set_retailprice"])}
|
||||
)
|
||||
our_set["pw_name"] = (
|
||||
f"{our_set['set_num']} - {our_set["set_theme"].upper()} {our_set['set_name']}"
|
||||
)
|
||||
if our_set["set_retailprice"] >= 110:
|
||||
our_set["requires_manual_price"] = True
|
||||
return our_set
|
||||
|
||||
elif response.status_code == 500:
|
||||
raise Exception(
|
||||
"Internal Server Error: Please check the API key and parameters."
|
||||
)
|
||||
else:
|
||||
raise Exception("Unexpected response from the API.")
|
||||
@@ -0,0 +1,221 @@
|
||||
import aiosqlite
|
||||
import logging
|
||||
from typing import Optional, List, Dict, Any
|
||||
import json
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Get the absolute path to edderkop.db in the project root
|
||||
DEFAULT_DB_PATH = os.path.join(
|
||||
os.path.dirname(os.path.dirname(__file__)), "edderkop.db"
|
||||
)
|
||||
|
||||
if not os.path.exists(DEFAULT_DB_PATH):
|
||||
raise FileNotFoundError(f"Database file not found at {DEFAULT_DB_PATH}")
|
||||
print('Please run "src/scripts/EDDERKOP.SQL" to setup the database.')
|
||||
os.exit(0)
|
||||
|
||||
|
||||
class AsyncSchedulerDB:
|
||||
"""
|
||||
Asynchronous SQLite database manager for APScheduler.
|
||||
Handles job storage, retrieval, and database operations using the main edderkop.db database.
|
||||
"""
|
||||
|
||||
def __init__(self, db_path: str = DEFAULT_DB_PATH):
|
||||
"""
|
||||
Initialize the database manager.
|
||||
|
||||
Args:
|
||||
db_path (str, optional): Path to the SQLite database file.
|
||||
Defaults to edderkop.db in project root.
|
||||
"""
|
||||
self.db_path = db_path
|
||||
self._initialized = False
|
||||
logger.info(f"Using database at: {self.db_path}")
|
||||
|
||||
async def initialize(self):
|
||||
"""Initialize the database schema if it doesn't exist."""
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
# Create apscheduler jobs table if it doesn't exist
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS apscheduler_jobs (
|
||||
id VARCHAR(191) NOT NULL,
|
||||
next_run_time FLOAT,
|
||||
job_state BLOB NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
self._initialized = True
|
||||
logger.info(f"Initialized scheduler database at {self.db_path}")
|
||||
|
||||
async def get_all_jobs(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Retrieve all jobs from the database.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: List of job dictionaries
|
||||
"""
|
||||
await self.initialize()
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(
|
||||
"SELECT * FROM apscheduler_jobs ORDER BY next_run_time"
|
||||
) as cursor:
|
||||
rows = await cursor.fetchall()
|
||||
return [
|
||||
{
|
||||
"id": row["id"],
|
||||
"next_run_time": row["next_run_time"],
|
||||
"job_state": json.loads(row["job_state"]),
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
async def get_job(self, job_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Retrieve a specific job by ID.
|
||||
|
||||
Args:
|
||||
job_id (str): The ID of the job to retrieve
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: Job dictionary if found, None otherwise
|
||||
"""
|
||||
await self.initialize()
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(
|
||||
"SELECT * FROM apscheduler_jobs WHERE id = ?", (job_id,)
|
||||
) as cursor:
|
||||
row = await cursor.fetchone()
|
||||
if row:
|
||||
return {
|
||||
"id": row["id"],
|
||||
"next_run_time": row["next_run_time"],
|
||||
"job_state": json.loads(row["job_state"]),
|
||||
}
|
||||
return None
|
||||
|
||||
async def add_job(
|
||||
self, job_id: str, next_run_time: float, job_state: Dict[str, Any]
|
||||
) -> bool:
|
||||
"""
|
||||
Add a new job to the database.
|
||||
|
||||
Args:
|
||||
job_id (str): Unique identifier for the job
|
||||
next_run_time (float): Unix timestamp for next scheduled run
|
||||
job_state (Dict[str, Any]): Job state dictionary
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
await self.initialize()
|
||||
try:
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute(
|
||||
"INSERT INTO apscheduler_jobs (id, next_run_time, job_state) VALUES (?, ?, ?)",
|
||||
(job_id, next_run_time, json.dumps(job_state)),
|
||||
)
|
||||
await db.commit()
|
||||
return True
|
||||
except aiosqlite.IntegrityError:
|
||||
logger.error(f"Job with ID {job_id} already exists")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding job: {e}")
|
||||
return False
|
||||
|
||||
async def update_job(
|
||||
self, job_id: str, next_run_time: float, job_state: Dict[str, Any]
|
||||
) -> bool:
|
||||
"""
|
||||
Update an existing job in the database.
|
||||
|
||||
Args:
|
||||
job_id (str): ID of the job to update
|
||||
next_run_time (float): New next run time
|
||||
job_state (Dict[str, Any]): Updated job state
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False if job not found
|
||||
"""
|
||||
await self.initialize()
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
cursor = await db.execute(
|
||||
"""UPDATE apscheduler_jobs
|
||||
SET next_run_time = ?, job_state = ?
|
||||
WHERE id = ?""",
|
||||
(next_run_time, json.dumps(job_state), job_id),
|
||||
)
|
||||
await db.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
async def remove_job(self, job_id: str) -> bool:
|
||||
"""
|
||||
Remove a job from the database.
|
||||
|
||||
Args:
|
||||
job_id (str): ID of the job to remove
|
||||
|
||||
Returns:
|
||||
bool: True if job was removed, False if not found
|
||||
"""
|
||||
await self.initialize()
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
cursor = await db.execute(
|
||||
"DELETE FROM apscheduler_jobs WHERE id = ?", (job_id,)
|
||||
)
|
||||
await db.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
async def remove_all_jobs(self) -> int:
|
||||
"""
|
||||
Remove all jobs from the database.
|
||||
|
||||
Returns:
|
||||
int: Number of jobs removed
|
||||
"""
|
||||
await self.initialize()
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
cursor = await db.execute("DELETE FROM apscheduler_jobs")
|
||||
await db.commit()
|
||||
return cursor.rowcount
|
||||
|
||||
async def get_due_jobs(self, now: float) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get all jobs that are due to run.
|
||||
|
||||
Args:
|
||||
now (float): Current timestamp
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: List of due jobs
|
||||
"""
|
||||
await self.initialize()
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(
|
||||
"""SELECT * FROM apscheduler_jobs
|
||||
WHERE next_run_time <= ?
|
||||
ORDER BY next_run_time""",
|
||||
(now,),
|
||||
) as cursor:
|
||||
rows = await cursor.fetchall()
|
||||
return [
|
||||
{
|
||||
"id": row["id"],
|
||||
"next_run_time": row["next_run_time"],
|
||||
"job_state": json.loads(row["job_state"]),
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
@@ -0,0 +1,116 @@
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
import dotenv
|
||||
from typing import Dict, Any
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
|
||||
class BricksetAPI:
|
||||
"""Class for interacting with the Brickset API"""
|
||||
|
||||
def __init__(self, debug: bool = False):
|
||||
self.username = os.getenv("BRICKSET_USERNAME")
|
||||
self.password = os.getenv("BRICKSET_PASS")
|
||||
self.api_key = os.getenv("BRICKSET_API_KEY")
|
||||
self.debug = debug
|
||||
self.headers = {"UserAgent": "Edderkop/0.1"}
|
||||
self.user_hash = None
|
||||
|
||||
def login(self) -> Dict[str, Any]:
|
||||
"""Login to Brickset API and get user hash"""
|
||||
url = "https://brickset.com/api/v3.asmx/login"
|
||||
params = {
|
||||
"username": self.username,
|
||||
"password": self.password,
|
||||
"apiKey": self.api_key,
|
||||
}
|
||||
|
||||
response = requests.get(url, params=params, headers=self.headers)
|
||||
if self.debug:
|
||||
print(f"[@DEBUG] Login response: {response.status_code}")
|
||||
print(f"[@DEBUG] Response content: {response.text}")
|
||||
|
||||
try:
|
||||
data = response.json()
|
||||
self.user_hash = data.get("hash")
|
||||
if not self.user_hash:
|
||||
raise ValueError("No hash received in login response")
|
||||
return data
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"[@ERROR] Failed to decode JSON response: {e}")
|
||||
print(f"[@DEBUG] Raw response: {response.text}")
|
||||
raise
|
||||
|
||||
def get_set(self, set_number: str) -> Dict[str, Any]:
|
||||
"""Get LEGO set information by set number"""
|
||||
if not self.user_hash:
|
||||
self.login()
|
||||
|
||||
url = "https://brickset.com/api/v3.asmx/getSets"
|
||||
params = {
|
||||
"apiKey": self.api_key,
|
||||
"userHash": self.user_hash,
|
||||
"params": json.dumps({"setNumber": f"{set_number}-1"}),
|
||||
}
|
||||
|
||||
response = requests.get(url, params=params, headers=self.headers)
|
||||
|
||||
if self.debug:
|
||||
print(f"[@DEBUG] Get set response: {response.status_code}")
|
||||
print(f"[@DEBUG] Response content: {response.text}")
|
||||
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
data = response.json()
|
||||
if not data.get("sets"):
|
||||
raise ValueError(f"No set data found for {set_number}")
|
||||
|
||||
set_data = data["sets"][0]
|
||||
our_set = {
|
||||
"set_num": set_data["number"],
|
||||
"set_name": set_data["name"],
|
||||
"set_image": set_data["image"]["thumbnailURL"],
|
||||
"set_barcode": set_data["barcode"]["UPC"],
|
||||
"set_retailprice": round(set_data["LEGOCom"]["US"]["retailPrice"]),
|
||||
}
|
||||
|
||||
# Calculate adjusted price (10% markup)
|
||||
retail_price = our_set["set_retailprice"]
|
||||
our_set["set_adjustredprice"] = retail_price + (
|
||||
(10 / 100) * retail_price
|
||||
)
|
||||
# Set Playwell inventory name
|
||||
if our_set["theme"] == "Duplo":
|
||||
our_set["pw_name"] = (
|
||||
f"{our_set['set_num']} - DUPLO {our_set['set_name']}"
|
||||
)
|
||||
else:
|
||||
our_set["pw_name"] = f"{our_set['set_num']} - {our_set['set_name']}"
|
||||
|
||||
return our_set
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
# print(f"[@ERROR] Failed to decode JSON response: {e}")
|
||||
# print(f"[@DEBUG] Raw response: {response.text}")
|
||||
raise
|
||||
except KeyError as e:
|
||||
# print(f"[@ERROR] Missing expected field in response: {e}")
|
||||
# print(f"[@DEBUG] Response data: {data}")
|
||||
raise
|
||||
elif response.status_code == 500:
|
||||
raise Exception(
|
||||
"Internal Server Error: Please check API key and parameters."
|
||||
)
|
||||
else:
|
||||
raise Exception(f"Unexpected response: {response.status_code}")
|
||||
|
||||
|
||||
def example():
|
||||
client = BricksetAPI(debug=True)
|
||||
try:
|
||||
set_info = client.get_set("10931")
|
||||
print(set_info)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
@@ -0,0 +1,16 @@
|
||||
import os
|
||||
import dotenv
|
||||
from square.environment import SquareEnvironment
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
LEGO_CAT_ID = os.getenv("SQUARE_LEGO_CAT_ID")
|
||||
|
||||
if os.getenv("SQUARE_ENV") == "production":
|
||||
SQUARE_APP_ID = os.getenv("PROD_SQUARE_APP_ID")
|
||||
SQUARE_TOKEN = os.getenv("PROD_SQUARE_TOKEN")
|
||||
environment = SquareEnvironment.PRODUCTION
|
||||
else:
|
||||
SQUARE_APP_ID = os.getenv("SANDBOX_SQUARE_APP_ID")
|
||||
SQUARE_TOKEN = os.getenv("SANDBOX_SQUARE_TOKEN")
|
||||
environment = SquareEnvironment.SANDBOX
|
||||
@@ -0,0 +1,66 @@
|
||||
import json
|
||||
|
||||
import get_sq_env
|
||||
|
||||
SQUARE_TOKEN = get_sq_env.SQUARE_TOKEN
|
||||
LEGO_CAT_ID = get_sq_env.LEGO_CAT_ID
|
||||
ENVIRONMENT = get_sq_env.environment
|
||||
|
||||
from square import Square
|
||||
from square.environment import SquareEnvironment
|
||||
|
||||
|
||||
def get_square_client():
|
||||
"""Initialize and return a Square client."""
|
||||
if not SQUARE_TOKEN:
|
||||
raise ValueError("SQUARE_TOKEN environment variable is not set.")
|
||||
square = Square(token=SQUARE_TOKEN, environment=ENVIRONMENT) # type: ignore
|
||||
return square
|
||||
|
||||
|
||||
client = get_square_client()
|
||||
|
||||
|
||||
def get_itm(item_id: str):
|
||||
"""Fetch an item from Square by its ID."""
|
||||
result = client.catalog.search_items(text_filter=item_id, limit=1)
|
||||
result_json = json.loads(result.model_dump_json())
|
||||
ids = {
|
||||
"item_id": result_json["items"][0]["id"],
|
||||
"vari_id": result_json["items"][0]["item_data"]["variations"][0][
|
||||
"item_variation_data"
|
||||
]["item_id"],
|
||||
}
|
||||
item = {
|
||||
"set_name": result_json["items"][0]["item_data"]["name"],
|
||||
"set_price": result_json["items"][0]["item_data"]["variations"][0][
|
||||
"item_variation_data"
|
||||
]["price_money"],
|
||||
"set_barcode": result_json["items"][0]["item_data"]["variations"][0][
|
||||
"item_variation_data"
|
||||
]["sku"],
|
||||
}
|
||||
return [ids, item]
|
||||
|
||||
|
||||
def cat2id(cat_id: str):
|
||||
"""Return a list of item IDs within the specified category ID."""
|
||||
result = client.catalog.search(
|
||||
object_types=["ITEM"],
|
||||
query={
|
||||
"exact_query": {
|
||||
"attribute_name": "category_id",
|
||||
"attribute_value": cat_id,
|
||||
}
|
||||
},
|
||||
)
|
||||
result_json = json.loads(result.model_dump_json())
|
||||
item_ids = [obj["id"] for obj in result_json.get("objects", [])]
|
||||
return item_ids
|
||||
|
||||
|
||||
def test() -> None:
|
||||
# all_lego_sets = cat2id(SQUARE_LEGO_CAT_ID) # type: ignore
|
||||
# result = get_itm("71433")
|
||||
# print(result)
|
||||
pass
|
||||
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE "REVIEW" (
|
||||
"sq_object" TEXT NOT NULL UNIQUE,
|
||||
"time_added" INTEGER,
|
||||
"set_number" INTEGER,
|
||||
"time_modified" INTEGER,
|
||||
PRIMARY KEY("sq_object")
|
||||
)
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import asyncio
|
||||
import aiohttp.web
|
||||
import uuid
|
||||
import os
|
||||
from base64 import b64decode
|
||||
import dotenv
|
||||
|
||||
dotenv.load_dotenv()
|
||||
SQUARE_LEGO_CAT_ID = os.environ.get("SQUARE_LEGO_CAT_ID")
|
||||
|
||||
from asyncio_throttle.throttler import Throttler
|
||||
|
||||
import asquare as sq
|
||||
import abricksetapi as alego
|
||||
|
||||
|
||||
def valid_uuid(val):
|
||||
try:
|
||||
uuid.UUID(str(val))
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
async def fullsync():
|
||||
"""'
|
||||
full_db = await sq.acat2id(SQUARE_LEGO_CAT_ID)
|
||||
for l_set in full_db:
|
||||
l_setinfo = await alego.throttled_get_set(l_set)
|
||||
await sq.update_itm(l_set, l_setinfo)"""
|
||||
|
||||
|
||||
async def stop():
|
||||
pass
|
||||
|
||||
|
||||
async def priceonly():
|
||||
pass
|
||||
|
||||
|
||||
async def handle_post(request):
|
||||
|
||||
valid_cmd = ["RUNFULLSYNC", "STOP", "RUNPRICEONLY"]
|
||||
|
||||
async def handle_cmd(cmd: str):
|
||||
if cmd not in valid_cmd:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
idp_key = request.headers.get("Idempotency-Key")
|
||||
user_agent = request.headers.get("User-Agent")
|
||||
|
||||
if not idp_key and not valid_uuid(idp_key) and user_agent != "Kopling/0.1":
|
||||
# 412 Precondition Failed
|
||||
# The server does not meet one of the preconditions that the requester put on the request header fields.
|
||||
return aiohttp.web.Response(status=412, text="412 Headers rejected!")
|
||||
|
||||
else:
|
||||
data = await request.post()
|
||||
reqdata = data.get(b64decode("CMD"))
|
||||
|
||||
if await handle_cmd(reqdata):
|
||||
match reqdata:
|
||||
case "RUNFULLSYNC":
|
||||
print("Running full sync...")
|
||||
await fullsync()
|
||||
return True
|
||||
case "STOP":
|
||||
await stop()
|
||||
return True
|
||||
case "RUNPRICEONLY":
|
||||
await priceonly()
|
||||
return True
|
||||
else:
|
||||
return aiohttp.web.Response(status=400, text="400 Bad Request.")
|
||||
result = {"message": f"Edderkop/0.1 received command: {reqdata}"}
|
||||
return aiohttp.web.json_response(status=200, data=result)
|
||||
|
||||
|
||||
app = aiohttp.web.Application()
|
||||
app.router.add_post("/", handle_post)
|
||||
|
||||
import subprocess
|
||||
|
||||
|
||||
def generate_toilet_banner(text, font="smblock", filter_effect=None, colors=None):
|
||||
command = ["toilet", "-f", font]
|
||||
if filter_effect:
|
||||
command.extend(["-F", filter_effect])
|
||||
if colors:
|
||||
# Toilet uses a specific format for color arguments, e.g., --gay or --metal
|
||||
# For general colors, you might need to use specific filters or direct color codes.
|
||||
# This example focuses on filters that inherently use colors.
|
||||
pass # More complex color handling would be needed for arbitrary colors
|
||||
|
||||
command.append(text)
|
||||
|
||||
try:
|
||||
result = subprocess.run(command, capture_output=True, text=True, check=True)
|
||||
return result.stdout
|
||||
except subprocess.CalledProcessError as e:
|
||||
return f"Error executing toilet: {e}\n{e.stderr}"
|
||||
|
||||
|
||||
async def main():
|
||||
throttler = Throttler(rate_limit=500, period=60) # 500 requests per minute
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(generate_toilet_banner("Edderkop v0.1"))
|
||||
print("Built for PLAY-WELL TEKNOLOGIES by contact@traps.moe\n\n")
|
||||
aiohttp.web.run_app(app, port=7432)
|
||||
@@ -0,0 +1,6 @@
|
||||
import src.libsquare as sq
|
||||
import src.bricksetapi as lego
|
||||
|
||||
oursq = sq.get_itm("71417")
|
||||
ourlego = lego.get_set(71417)
|
||||
print(f"{oursq}\n\n{ourlego}\n\n")
|
||||
@@ -0,0 +1,39 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from uuid import uuid4
|
||||
from base64 import b64encode
|
||||
import os
|
||||
|
||||
|
||||
async def idp_request(cmd: str):
|
||||
headers = {
|
||||
"Idempotency-Key": f"{uuid4()}",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Kopling/0.12",
|
||||
}
|
||||
print(f"[@DEBUG] Idempotency-Key: {headers['Idempotency-Key']}")
|
||||
|
||||
enc_cmd = b64encode(f"{cmd}".encode("utf-8")).decode("utf-8")
|
||||
print("[@DEBUG] Encoded command:", enc_cmd)
|
||||
print("[@DEBUG] Sending request..")
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
"http://localhost:7432", # Added http:// protocol
|
||||
headers=headers,
|
||||
json={"CMD": enc_cmd}, # Changed to json instead of data
|
||||
) as resp:
|
||||
print(f"[@DEBUG] Response status: {resp.status}")
|
||||
print(f"[@DEBUG] Response body: {await resp.text()}")
|
||||
except Exception as e:
|
||||
print(f"\nRequest failed: {e}")
|
||||
os._exit(-1)
|
||||
|
||||
|
||||
async def main():
|
||||
# Await the task instead of just creating it
|
||||
await idp_request("RUNUPDATE")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,121 @@
|
||||
import asyncio
|
||||
import json
|
||||
from aiohttp import web
|
||||
from base64 import b64decode
|
||||
from typing import Dict, Any
|
||||
import uuid
|
||||
from time import time
|
||||
import dotenv
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
Path("./logs/").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
import banner
|
||||
import get_sq_env
|
||||
import asquare as sq
|
||||
from abricksetapi import AsyncBricksetAPI
|
||||
|
||||
lego = AsyncBricksetAPI(debug=True)
|
||||
|
||||
dotenv.load_dotenv()
|
||||
SQUARE_LEGO_CAT_ID = os.environ.get("SQUARE_LEGO_CAT_ID")
|
||||
|
||||
|
||||
from asyncio_throttle.throttler import Throttler
|
||||
|
||||
logger = logging.getLogger("aiohttp.access")
|
||||
logging.basicConfig(
|
||||
filename="./logs/edderkop.log", encoding="utf-8", level=logging.DEBUG
|
||||
)
|
||||
|
||||
UA = re.compile(r"^kopling\/?", re.IGNORECASE)
|
||||
|
||||
|
||||
async def run_update():
|
||||
set_list = await sq.acat2id(SQUARE_LEGO_CAT_ID)
|
||||
for l_set in set_list:
|
||||
print(f"[@DEBUG]{l_set}")
|
||||
l_setinfo = await lego.throttled_get_set(l_set["number"])
|
||||
print(f"[@DEBUG]{l_setinfo}")
|
||||
# await sq.update_itm(l_set, l_setinfo)
|
||||
|
||||
|
||||
def valid_uuid(val):
|
||||
try:
|
||||
uuid.UUID(str(val))
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
async def request_handler(request: web.Request) -> web.Response:
|
||||
"""Handle incoming POST requests with command processing"""
|
||||
try:
|
||||
# Verify request method
|
||||
if request.method != "POST":
|
||||
logger.error(
|
||||
f"{time()}\tInvalid request method from %s at ", request.remote
|
||||
)
|
||||
return web.Response(status=405, text="Method not allowed. Use POST.")
|
||||
|
||||
# Get JSON data from request
|
||||
try:
|
||||
data = await request.json()
|
||||
except json.JSONDecodeError:
|
||||
logger.error("Invalid JSON from %s", request.remote)
|
||||
return web.Response(
|
||||
status=400, text="{time()}\tInvalid JSON in request body"
|
||||
)
|
||||
# Validate Idempotency-Key header
|
||||
if valid_uuid(request.headers.get("Idempotency-Key")) is False:
|
||||
logger.error("{time()}\tInvalid Idempotency-Key from %s", request.remote)
|
||||
return web.Response(status=412, text="412 Headers rejected!")
|
||||
# Validate User-Agent header
|
||||
if not re.match(UA, str(request.headers.get("User-Agent"))):
|
||||
logger.error("{time()}\tInvalid User-Agent from %s", request.remote)
|
||||
return web.Response(status=412, text="412 Headers rejected!")
|
||||
# Validate command exists
|
||||
if "CMD" not in data:
|
||||
logger.error("{time()}\tMissing CMD field from %s", request.remote)
|
||||
return web.Response(status=400, text="Missing CMD field in request")
|
||||
|
||||
# Decode and process command
|
||||
try:
|
||||
decoded_cmd = b64decode(data["CMD"]).decode("utf-8")
|
||||
print(
|
||||
f"""At: {time()}\nReceived command: {decoded_cmd}\nUser-Agent: {request.headers.get('User-Agent')}\nIdempotency-Key: {request.headers.get('Idempotency-Key')}"""
|
||||
)
|
||||
logging.info(
|
||||
f"{time()}\tCommand: {decoded_cmd}\tFrom: {request.remote}\tUA: {request.headers.get('User-Agent')}\tIdempotency-Key: {request.headers.get('Idempotency-Key')}"
|
||||
)
|
||||
|
||||
# Process the command
|
||||
if decoded_cmd == "RUNUPDATE":
|
||||
# Add your command handling logic here
|
||||
await run_update()
|
||||
response_data = {"status": "success", "message": "Update started"}
|
||||
logging.info(f"{time()}\tUpdate command processed successfully")
|
||||
|
||||
return web.json_response(response_data)
|
||||
else:
|
||||
return web.Response(status=400, text=f"Unknown command: {decoded_cmd}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing command: {str(e)}")
|
||||
return web.Response(status=500, text=f"Server error: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Request handler error: {str(e)}")
|
||||
return web.Response(status=500, text="Internal server error")
|
||||
|
||||
|
||||
app = web.Application()
|
||||
app.router.add_post("/", request_handler)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(banner.generate("Edderkop v0.1"))
|
||||
print("Built for PLAY-WELL TEKNOLOGIES by contact@traps.moe\n\n")
|
||||
web.run_app(app, host="localhost", port=7432)
|
||||
@@ -0,0 +1,15 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
|
||||
from asyncio_throttle.throttler import Throttler
|
||||
|
||||
throttler = Throttler(rate_limit=500, period=60) # 500 requests per minute
|
||||
|
||||
|
||||
async def worker(throttler, session):
|
||||
while True:
|
||||
async with throttler:
|
||||
async with session.get("http://example.com") as resp:
|
||||
do_some_job_with(await resp.text())
|
||||
|
||||
await asyncio.sleep(0.05)
|
||||
Reference in new issue
Block a user