40 Commits

Author SHA1 Message Date
3ec9fef3cf Merge branch 'new-stuff'
All checks were successful
Deploy App to Docker / deploy (push) Successful in 1m10s
2025-04-11 11:34:55 -04:00
9e656ef329 this is a bug - apollo 2025-04-11 11:33:03 -04:00
cc829c3b54 asdf
All checks were successful
Deploy App to Docker / deploy (push) Successful in 2m27s
2025-04-10 11:35:12 -04:00
105c02a3de a
All checks were successful
Deploy App to Docker / deploy (push) Successful in 20s
2025-04-08 13:39:56 -04:00
4f56eec551 a
All checks were successful
Deploy App to Docker / deploy (push) Successful in 21s
2025-04-08 13:35:03 -04:00
029c28166e asdf
All checks were successful
Deploy App to Docker / deploy (push) Successful in 31s
2025-04-08 13:26:12 -04:00
1bb842ea3f f
All checks were successful
Deploy App to Docker / deploy (push) Successful in 22s
2025-04-08 11:26:18 -04:00
9603a7e58f f
All checks were successful
Deploy App to Docker / deploy (push) Successful in 2m33s
2025-04-08 10:48:18 -04:00
3dc955698e asdf
All checks were successful
Deploy App to Docker / deploy (push) Successful in 31s
2025-04-07 17:55:33 -04:00
eadff66eb6 fd
All checks were successful
Deploy App to Docker / deploy (push) Successful in 19s
2025-04-07 17:17:44 -04:00
6ded276269 f
All checks were successful
Deploy App to Docker / deploy (push) Successful in 20s
2025-04-07 17:14:20 -04:00
168313d882 f
All checks were successful
Deploy App to Docker / deploy (push) Successful in 24s
2025-04-07 17:09:07 -04:00
57ac76386b f
All checks were successful
Deploy App to Docker / deploy (push) Successful in 20s
2025-04-07 16:57:57 -04:00
a1f15d6e6a s
All checks were successful
Deploy App to Docker / deploy (push) Successful in 41s
2025-04-07 16:53:51 -04:00
11ed680347 f
All checks were successful
Deploy App to Docker / deploy (push) Successful in 19s
2025-04-07 16:53:34 -04:00
fff6007a10 ft
All checks were successful
Deploy App to Docker / deploy (push) Successful in 23s
2025-04-07 16:49:18 -04:00
846a44e5fc s
All checks were successful
Deploy App to Docker / deploy (push) Successful in 2m7s
2025-04-07 16:18:32 -04:00
9360bc9f9a s
Some checks failed
Deploy App to Docker / deploy (push) Failing after 19s
2025-04-07 16:17:28 -04:00
48fa6bfaa9 dockerfile
Some checks failed
Deploy App to Docker / deploy (push) Failing after 19s
2025-04-07 16:15:50 -04:00
c151d41c43 reqs
All checks were successful
Deploy App to Docker / deploy (push) Successful in 1m17s
2025-04-07 16:10:48 -04:00
9021151b74 addresses and pricing
All checks were successful
Deploy App to Docker / deploy (push) Successful in 1m30s
2025-04-07 16:08:18 -04:00
f59f0f350c s
All checks were successful
Deploy App to Docker / deploy (push) Successful in 16s
2025-04-07 12:53:52 -04:00
478ce4ec41 a
All checks were successful
Deploy App to Docker / deploy (push) Successful in 20s
2025-04-07 12:50:51 -04:00
18ceef8351 d
All checks were successful
Deploy App to Docker / deploy (push) Successful in 3m3s
2025-04-07 11:56:34 -04:00
87c84fd0a8 g 2025-04-07 11:54:24 -04:00
0c78276b12 j 2025-04-07 11:54:02 -04:00
47a1b1d3ac actions? 2025-04-07 11:52:35 -04:00
c889a84c34 h 2025-04-06 18:59:37 -04:00
1ef706afe5 a 2025-04-06 18:59:21 -04:00
3454f24451 a 2025-04-06 18:50:11 -04:00
7786655db0 time 2025-04-06 18:49:06 -04:00
ed52e7da04 json? 2025-04-06 18:48:47 -04:00
bf3f4ddb38 clean 2025-04-06 18:41:07 -04:00
3f53513c36 fixy req 2025-04-06 18:38:32 -04:00
d3bd696d67 1 more 2025-04-06 18:23:52 -04:00
113a920da7 fix order 2025-04-06 18:23:31 -04:00
9d11adaf6c hourly orders 2025-04-06 18:20:24 -04:00
8de5bec523 a 2025-04-06 15:07:45 -04:00
0414018099 asdf 2025-04-06 14:44:18 -04:00
d4579a9db0 a 2025-04-06 14:39:52 -04:00
12 changed files with 508 additions and 72 deletions

View File

@ -0,0 +1,32 @@
# .gitea/workflows/deploy.yml
name: Deploy App to Docker
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Docker
uses: docker/setup-buildx-action@v1
with:
version: 'latest'
- name: Build Docker Image
run: |
docker build -t giga_tcg .
- name: Remove existing Docker container
run: |
docker rm -f giga_tcg_container || true
- name: Run Docker container
run: |
docker run -d -v /mnt/user/appdata/gigatcg/giga_tcg/cookies:/app/cookies -v /mnt/user/appdata/gigatcg/tmp:/app/tmp -p 8000:8000 --name giga_tcg_container giga_tcg

View File

@ -4,6 +4,20 @@ WORKDIR /app
ENV DATABASE_URL=postgresql://poggers:giga!@192.168.1.41:5432/omegatcgdb
RUN apt-get update && apt-get install -y \
libglib2.0-0 \
libpango-1.0-0 \
libcairo2 \
libjpeg62-turbo \
libgif7 \
libxml2 \
fonts-liberation \
libharfbuzz0b \
libfribidi0 \
libgtk-3-0 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

View File

@ -1,2 +1,3 @@
# giga_tcg
test

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,87 @@
<!DOCTYPE html>
<html>
<head>
<style>
/* Setting up the page size for landscape orientation - 6x4 inches */
@page {
size: 6in 4in; /* Landscape orientation */
margin: 0;
}
/* Force page breaks after each label */
body {
margin: 0;
padding: 0;
width: 6in; /* Adjusted for landscape */
height: 4in; /* Adjusted for landscape */
position: relative;
font-family: Arial, sans-serif;
}
.label-container {
width: 100%;
height: 100%;
position: relative;
box-sizing: border-box;
padding: 0.25in;
page-break-after: always; /* Ensures a page break after each label */
}
/* Return address image in top left */
.return-address {
position: absolute;
top: 0.25in;
left: 0.25in;
width: 2.75in;
height: 0.85in;
}
/* Stamp area in top right */
.stamp-area {
position: absolute;
top: 0.25in;
right: 0.25in;
width: 0.8in;
height: 0.9in;
border: 1px dashed #999;
display: flex;
justify-content: center;
align-items: center;
}
.stamp-text {
font-size: 8pt;
color: #999;
text-align: center;
}
/* Main address centered in the middle */
.address {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 3in;
text-align: center;
font-size: 12pt;
line-height: 1.5;
}
</style>
</head>
<body>
<div class="label-container">
<img src="{{ return_address_path }}" class="return-address" alt="Return Address">
<div class="stamp-area">
<span class="stamp-text">PLACE<br>STAMP<br>HERE</span>
</div>
<div class="address">
{{ recipient_name }}<br>
{{ address_line1 }}<br>
{% if address_line2 %}{{ address_line2 }}<br>{% endif %}
{{ city }}, {{ state }} {{ zip_code }}
</div>
</div>
</body>
</html>

View File

@ -318,7 +318,8 @@ async def update_cookies(
)
class TCGPlayerOrderRequest(BaseModel):
order_ids: List[str]
# optional
order_ids: Optional[List[str]] = None
@router.post("/processOrders", response_model=ProcessOrdersResponse)
async def process_orders(
@ -327,7 +328,7 @@ async def process_orders(
) -> ProcessOrdersResponse:
"""Process TCGPlayer orders."""
try:
orders = tcgplayer_api_service.process_orders(body.order_ids)
orders = tcgplayer_api_service.process_open_orders(body.order_ids)
return ProcessOrdersResponse(
status_code=200,
success=True,

View File

@ -8,10 +8,14 @@ from app.db.utils import db_transaction
from typing import List, Dict
from decimal import Decimal, ROUND_HALF_UP
import pandas as pd
import numpy as np
import logging
logger = logging.getLogger(__name__)
ACTIVE_PRICING_ALGORITHIM = 'tcgplayer_recommended_algo' # 'default_pricing_algo' or 'tcgplayer_recommended_algo'
FREE_SHIPPING = True
class PricingService:
def __init__(self, db: Session, file_service: FileService, tcgplayer_service: TCGPlayerService):
@ -115,6 +119,98 @@ class PricingService:
row[price_type] = price
return row
def smooth_markup(price, markup_bands):
"""
Applies a smoothed markup based on the given price and markup bands.
Uses numpy for smooth transitions.
"""
# Convert markup bands to lists for easy lookup
markups = np.array(list(markup_bands.keys()))
min_prices = np.array([x[0] for x in markup_bands.values()])
max_prices = np.array([x[1] for x in markup_bands.values()])
# Find the index of the price's range
idx = np.where((min_prices <= price) & (max_prices >= price))[0]
if len(idx) > 0:
# If price is within a defined range, return the markup
markup = markups[idx[0]]
else:
# If price is not directly within any range, check smooth transitions
# Find the closest two bands for interpolation
idx_lower = np.argmax(max_prices <= price) # Closest range below the price
idx_upper = np.argmax(min_prices > price) # Closest range above the price
if idx_lower != idx_upper:
# Linear interpolation between the two neighboring markups
price_diff = (price - max_prices[idx_lower]) / (min_prices[idx_upper] - max_prices[idx_lower])
markup = np.interp(price_diff, [0, 1], [markups[idx_lower], markups[idx_upper]])
# Apply the markup to the price
return price * markup
def tcgplayer_recommended_algo(self, row: pd.Series) -> pd.Series:
# Convert input values to Decimal for precise arithmetic
tcg_low = Decimal(str(row.get('tcg_low_price'))) if not pd.isna(row.get('tcg_low_price')) else None
tcg_low_shipping = Decimal(str(row.get('tcg_low_price_with_shipping'))) if not pd.isna(row.get('tcg_low_price_with_shipping')) else None
tcg_market_price = Decimal(str(row.get('tcg_market_price'))) if not pd.isna(row.get('tcg_market_price')) else None
current_price = Decimal(str(row.get('tcg_marketplace_price'))) if not pd.isna(row.get('tcgplayer_marketplace_price')) else None
total_quantity = str(row.get('total_quantity')) if not pd.isna(row.get('total_quantity')) else "0"
added_quantity = str(row.get('add_to_quantity')) if not pd.isna(row.get('add_to_quantity')) else "0"
quantity = int(total_quantity) + int(added_quantity)
if tcg_market_price is None:
logger.warning(f"Missing pricing data for row: {row}")
row['new_price'] = None
return row
# Define precision for rounding
TWO_PLACES = Decimal('0.01')
# Apply pricing rules
markup_bands = {
2.53: (Decimal('0.01'), Decimal('0.50')),
1.42: (Decimal('0.51'), Decimal('1.00')),
1.29: (Decimal('1.01'), Decimal('3.00')),
1.17: (Decimal('3.01'), Decimal('20.00')),
1.07: (Decimal('20.01'), Decimal('35.00')),
1.05: (Decimal('35.01'), Decimal('50.00')),
1.03: (Decimal('50.01'), Decimal('100.00')),
1.02: (Decimal('100.01'), Decimal('200.00')),
1.01: (Decimal('200.01'), Decimal('1000.00'))
}
if quantity > 3:
quantity_markup = Decimal('0.1')
for markup in markup_bands:
markup = markup + quantity_markup
quantity_markup = quantity_markup - Decimal('0.01')
if FREE_SHIPPING:
free_shipping_markup = Decimal('0.05')
for markup in markup_bands:
markup = markup + free_shipping_markup
free_shipping_markup = free_shipping_markup - Decimal('0.005')
# Apply the smoothed markup
new_price = self.smooth_markup(tcg_market_price, markup_bands)
if tcg_low_shipping is not None and tcg_low_shipping < new_price:
new_price = tcg_low_shipping
if new_price < Decimal('0.25'):
new_price = Decimal('0.25')
if current_price / new_price > Decimal('0.25'):
logger.warning(f"Price drop too large for row: {row}")
new_price = current_price
# Ensure exactly 2 decimal places
new_price = new_price.quantize(TWO_PLACES, rounding=ROUND_HALF_UP)
# Convert back to float or string as needed for your dataframe
row['new_price'] = float(new_price)
return row
def default_pricing_algo(self, row: pd.Series) -> pd.Series:
"""Default pricing algorithm with complex pricing rules"""
@ -167,8 +263,13 @@ class PricingService:
def apply_pricing_algo(self, row: pd.Series, pricing_algo: callable = None) -> pd.Series:
"""Modified to handle the pricing algorithm as an instance method"""
if pricing_algo is None:
if pricing_algo is None or ACTIVE_PRICING_ALGORITHIM == 'default_pricing_algo':
pricing_algo = self.default_pricing_algo
elif ACTIVE_PRICING_ALGORITHIM == 'tcgplayer_recommended_algo':
pricing_algo = self.tcgplayer_recommended_algo
else:
pricing_algo = self.default_pricing_algo
return pricing_algo(row)
def generate_tcgplayer_inventory_update_file_with_pricing(self, open_box_ids: List[str] = None) -> bytes:
@ -198,8 +299,13 @@ class PricingService:
.all()
}
# Map the ids using the dictionary
df['product_id'] = df['tcgplayer_id'].map(product_id_mapping)
# Map tcgplayer_id to product_id, ensure strings, keep None if not found
df['product_id'] = df['tcgplayer_id'].map(product_id_mapping).apply(lambda x: str(x) if pd.notnull(x) else None)
# Log any tcgplayer_ids that didn't map to a product_id
null_product_ids = df[df['product_id'].isnull()]['tcgplayer_id'].tolist()
if null_product_ids:
logger.warning(f"The following tcgplayer_ids could not be mapped to a product_id: {null_product_ids}")
price_lookup = self.get_all_prices_for_products(df['product_id'].unique())

View File

@ -26,6 +26,7 @@ class TaskService:
def register_scheduled_tasks(self):
self.scheduler.add_job(self.hourly_pricing, 'cron', minute='36')
self.scheduler.add_job(self.hourly_orders, 'cron', hour='*', minute='03')
# every 5 hours on the 24th minute
#self.scheduler.add_job(self.inventory_pricing, 'cron', hour='*', minute='44')
self.logger.info("Scheduled tasks registered.")
@ -35,6 +36,11 @@ class TaskService:
self.pricing_service.cron_load_prices()
self.logger.info("Finished hourly pricing task")
def hourly_orders(self):
self.logger.info("Running hourly orders task")
self.tcgplayer_api_service.process_orders_task()
self.logger.info("Finished hourly orders task")
def inventory_pricing(self):
self.tcgplayer_api_service.cron_tcgplayer_api_pricing()

View File

@ -7,7 +7,12 @@ from app.db.utils import db_transaction
from sqlalchemy.orm import Session
from datetime import datetime
from uuid import uuid4 as uuid
from jinja2 import Environment, FileSystemLoader
from weasyprint import HTML
import json
import time
import re
import csv
logger = logging.getLogger(__name__)
@ -16,6 +21,7 @@ class TCGPlayerAPIConfig:
"""Configuration for TCGPlayer API"""
ORDER_BASE_URL: str = "https://order-management-api.tcgplayer.com/orders"
API_VERSION: str = "?api-version=2.0"
SELLER_KEY: str = "e576ed4c"
class TCGPlayerAPIService:
def __init__(self, db: Session):
@ -26,6 +32,10 @@ class TCGPlayerAPIService:
self.config = TCGPlayerAPIConfig()
self.cookies = self.get_cookies()
self.session = None
self.template_dir = "/app/app/assets/templates"
self.env = Environment(loader=FileSystemLoader(self.template_dir))
self.address_label_template = self.env.get_template("address_label.html")
self.return_address_png = "file:///app/app/assets/images/ccrcardsaddress.png"
def get_cookies(self) -> dict:
if self.is_in_docker:
@ -40,6 +50,25 @@ class TCGPlayerAPIService:
return response.json()
return None
def get_orders(self, size: int = 25) -> dict:
url = f"{self.config.ORDER_BASE_URL}/search{self.config.API_VERSION}"
payload = {
"searchRange": "LastThreeMonths",
"filters": {
"sellerKey": self.config.SELLER_KEY
},
"sortBy": [
{"sortingType": "orderStatus", "direction": "ascending"},
{"sortingType": "orderDate", "direction": "descending"}
],
"from": 0,
"size": size
}
response = self.requests_util.send_request(url, method='POST', cookies=self.cookies, json=payload)
if response:
return response.json()
return None
def get_product_ids_from_sku(self, sku_ids: list[str]) -> dict:
"""Get product IDs from TCGPlayer SKU IDs"""
# convert SKU IDs to integers
@ -101,14 +130,30 @@ class TCGPlayerAPIService:
self.db.add_all(order_products)
return db_order
def process_orders(self, orders: list[str]):
processed_orders = []
for order_id in orders:
order = self.get_order(order_id)
if order:
self.save_order(order)
processed_orders.append(order_id)
return processed_orders
def process_orders_task(self):
# get last 25 orders from tcgplayer
orders = self.get_orders(size=100)
if orders:
# get list of order ids
order_ids = [order['orderNumber'] for order in orders['orders']]
# get a list of order ids that are not in the database
existing_orders = self.db.query(Orders).filter(Orders.order_id.in_(order_ids)).all()
existing_order_ids = [order.order_id for order in existing_orders]
# get a list of order ids that are not in the database
new_order_ids = [order_id for order_id in order_ids if order_id not in existing_order_ids]
# process new orders
processed_orders = []
if new_order_ids:
logger.info(f"Processing {len(new_order_ids)} new orders")
new_orders = [order for order in orders['orders'] if order['orderNumber'] in new_order_ids]
for new_order in new_orders:
order = self.get_order(new_order['orderNumber'])
self.save_order(order)
processed_orders.append(order['orderNumber'])
logger.info(f"Processed {len(processed_orders)} new orders")
return processed_orders
else:
logger.info("No new orders to process")
def get_scryfall_data(self, scryfall_id: str):
url = f"https://api.scryfall.com/cards/{scryfall_id}?format=json"
@ -133,7 +178,6 @@ class TCGPlayerAPIService:
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0 Safari/537.36'
}
url = f"https://mp-search-api.tcgplayer.com/v2/product/{tcgplayer_id}/details?mpfev=3279"
#self.requests_util.rate_limit()
response = self.session.get(url, headers=headers)
self.requests_util.previous_request_time = datetime.now()
return response
@ -187,6 +231,161 @@ class TCGPlayerAPIService:
if pricing_data:
self.save_tcgplayer_pricing_data(card.product_id, pricing_data)
def get_packing_slip_pdf_for_orders(self, order_ids: list[str]):
url = f"{self.config.ORDER_BASE_URL}/packing-slips/export{self.config.API_VERSION}"
payload = {
"sortingType": "byRelease",
"format": "default",
"timezoneOffset": -4,
"orderNumbers": order_ids
}
response = self.requests_util.send_request(url, method='POST', cookies=self.cookies, json=payload)
if response:
# get filename from response headers
header = response.headers.get('Content-Disposition', '')
match = re.search(r'filename="?([^";]+)"?', header)
filename = match.group(1) if match else f'packingslip{datetime.now().strftime("%Y%m%d_%H%M%S")}.pdf'
output_filename = f'/app/tmp/{filename}'
# save file to disk
with open(output_filename, 'wb') as f:
f.write(response.content)
return output_filename
def get_pull_sheet_for_orders(self, order_ids: list[str]):
url = f"{self.config.ORDER_BASE_URL}/pull-sheets/export{self.config.API_VERSION}"
payload = {
"orderNumbers": order_ids,
"timezoneOffset": -4,
}
response = self.requests_util.send_request(url, method='POST', cookies=self.cookies, json=payload)
if response:
# get filename from response headers
header = response.headers.get('Content-Disposition', '')
match = re.search(r'filename="?([^";]+)"?', header)
filename = match.group(1) if match else f'packingslip{datetime.now().strftime("%Y%m%d_%H%M%S")}.pdf'
output_filename = f'/app/tmp/{filename}'
# save file to disk
with open(output_filename, 'wb') as f:
f.write(response.content)
return output_filename
def get_address_labels_csv(self, order_ids: list[str]):
url = f"{self.config.ORDER_BASE_URL}/shipping/export{self.config.API_VERSION}"
payload = {
"orderNumbers": order_ids,
"timezoneOffset": -4
}
response = self.requests_util.send_request(url, method='POST', cookies=self.cookies, json=payload)
if response:
# get filename from response headers
header = response.headers.get('Content-Disposition', '')
match = re.search(r'filename="?([^";]+)"?', header)
filename = match.group(1) if match else f'shipping{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'
output_filename = f'/app/tmp/{filename}'
# save file to disk
with open(output_filename, 'wb') as f:
f.write(response.content)
return output_filename
def get_address_labels_pdf(self, order_ids: list[str]):
shipping_csv_filename = self.get_address_labels_csv(order_ids)
with open(shipping_csv_filename, 'r') as f:
reader = csv.DictReader(f)
orders = {}
for row in reader:
order_id = row.pop('Order #')
orders[order_id] = row
labels_html = []
for order_id in orders:
order = orders[order_id]
#if float(order['Value of Products']) >49.99:
#continue
# Extract relevant information from the order
order_info = {
"recipient_name": order['FirstName'] + ' ' + order['LastName'],
"address_line1": order['Address1'],
"address_line2": order['Address2'] if 'Address2' in order else '',
"city": order['City'],
"state": order['State'],
"zip_code": order['PostalCode'],
"return_address_path": self.return_address_png
}
# Render the label HTML using the template
labels_html.append(self.address_label_template.render(order_info))
if labels_html:
# Combine the rendered labels into one HTML string
full_html = "<html><body>" + "\n".join(labels_html) + "</body></html>"
# Generate a unique output filename with a timestamp
output_filename = f'/app/tmp/address_labels_{datetime.now().strftime("%Y%m%d_%H%M%S")}.pdf'
# Generate the PDF from the HTML string
HTML(string=full_html).write_pdf(output_filename)
return output_filename
else:
print("No orders found or no valid labels generated.")
return None
def process_open_orders(self, order_ids: list[str]=None):
# get all open orders
url = f"{self.config.ORDER_BASE_URL}/search{self.config.API_VERSION}"
"""{"searchRange":"LastThreeMonths","filters":{"sellerKey":"e576ed4c","orderStatuses":["Processing","ReadyToShip","Received","Pulling","ReadyForPickup"],"fulfillmentTypes":["Normal"]},"sortBy":[{"sortingType":"orderStatus","direction":"ascending"},{"sortingType":"orderDate","direction":"ascending"}],"from":0,"size":25}"""
payload = {
"searchRange": "LastThreeMonths",
"filters": {
"sellerKey": self.config.SELLER_KEY,
"orderStatuses": ["Processing", "ReadyToShip", "Received", "Pulling", "ReadyForPickup"],
"fulfillmentTypes": ["Normal"]
},
"sortBy": [
{"sortingType": "orderStatus", "direction": "ascending"},
{"sortingType": "orderDate", "direction": "ascending"}
],
"from": 0,
"size": 100
}
response = self.requests_util.send_request(url, method='POST', cookies=self.cookies, json=payload)
if response:
orders = response.json()
if orders and 'orders' in orders:
if order_ids is None:
order_ids = [order['orderNumber'] for order in orders['orders']]
# get packing slip pdf
packing_slip_filename = self.get_packing_slip_pdf_for_orders(order_ids)
# get pull sheet pdf
pull_sheet_filename = self.get_pull_sheet_for_orders(order_ids)
# get address labels pdf
address_labels_filename = self.get_address_labels_pdf(order_ids)
with open(packing_slip_filename, 'rb') as packing_slip_file, \
open(pull_sheet_filename, 'rb') as pull_sheet_file, \
open(address_labels_filename, 'rb') as address_labels_file:
files = [
#packing_slip_file,
# pull_sheet_file,
address_labels_file
]
# request post pdfs
for file in files:
self.requests_util.bare_request(
url="http://192.168.1.110:8000/upload",
method='POST',
files={'file': file}
)
time.sleep(10)
return order_ids
return None
# this one contains nearly everything, use it first

View File

@ -61,10 +61,14 @@ class RequestHeaders:
'origin': Headers.SELLER_ORIGIN,
'referer': Headers.SELLER_REFERER
}
POST_HEADERS = {
'content-type': 'application/json'
}
class URLHeaders:
# combine base and seller headers
ORDER_HEADERS = {**RequestHeaders.BASE_HEADERS, **RequestHeaders.SELLER_HEADERS}
POST_HEADERS = {**RequestHeaders.BASE_HEADERS, **RequestHeaders.SELLER_HEADERS, **RequestHeaders.POST_HEADERS}
class RequestsUtil:
def __init__(self, browser_type: Browser = Browser.BRAVE):
@ -79,10 +83,10 @@ class RequestsUtil:
session.cookies.update(cookies)
return session
def bare_request(self, url: str, method: str, cookies: dict = None, data=None) -> requests.Response:
def bare_request(self, url: str, method: str, cookies: dict = None, data=None, files=None) -> requests.Response:
"""Send a request without any additional processing"""
try:
response = requests.request(method, url, cookies=cookies, data=data)
response = requests.request(method, url, cookies=cookies, data=data, files=files)
response.raise_for_status()
return response
except requests.RequestException as e:
@ -113,19 +117,18 @@ class RequestsUtil:
"""Rate limit requests by waiting for a specified time between requests"""
time_diff = (datetime.now() - self.previous_request_time).total_seconds()
if time_diff < time_between_requests:
# logger.info(f"Waiting {time_between_requests - time_diff} seconds before next request...")
time.sleep(time_between_requests - time_diff)
def send_request(self, url: str, method: str, cookies: dict, data=None) -> requests.Response:
def send_request(self, url: str, method: str, cookies: dict, data=None, json=None) -> requests.Response:
"""Send a request with the specified cookies"""
headers = self.set_headers(url)
headers = self.set_headers(url, method)
if not headers:
raise ValueError("Headers not set")
try:
self.rate_limit()
response = requests.request(method, url, headers=headers, cookies=cookies, data=data)
response = requests.request(method, url, headers=headers, cookies=cookies, data=data, json=json)
response.raise_for_status()
self.previous_request_time = datetime.now()
@ -135,57 +138,12 @@ class RequestsUtil:
logger.error(f"Request failed: {str(e)}")
return None
def set_headers(self, url: str):
def set_headers(self, url: str, method: str) -> Dict:
# use tcgplayerendpoints enum to set headers where url partially matches enum value
for endpoint in TCGPlayerEndpoints:
if endpoint.value in url:
if endpoint.value in url and str.upper(method) == "POST":
return URLHeaders.POST_HEADERS
elif endpoint.value in url:
return URLHeaders.ORDER_HEADERS
else:
raise ValueError(f"Endpoint not found in TCGPlayerEndpoints: {url}")
def old_set_headers(self, method: str) -> Dict:
base_headers = {
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'accept-language': 'en-US,en;q=0.8',
'priority': 'u=0, i',
'referer': 'https://store.tcgplayer.com/admin/pricing',
'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Brave";v="132"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
'sec-fetch-dest': 'document',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'same-origin',
'sec-fetch-user': '?1',
'sec-gpc': '1',
'upgrade-insecure-requests': '1',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36'
}
if method == 'POST':
post_headers = {
'cache-control': 'max-age=0',
'content-type': 'application/x-www-form-urlencoded',
'origin': 'https://store.tcgplayer.com'
}
base_headers.update(post_headers)
return base_headers
"""
curl 'https://order-management-api.tcgplayer.com/orders/E576ED4C-38871F-B0277?api-version=2.0' \
-H 'accept: application/json, text/plain, */*' \
-H 'accept-language: en-US,en;q=0.8' \
-b 'tcgpartner=PK=TRADECARDS&M=1; valid=set=true; product-display-settings=sort=price+shipping&size=10; TCG_Data=M=1&SearchGameNameID=magic; tcg-uuid=ab16b5f8-dd66-446d-b217-d394328a5cf1; setting=CD=US&M=1; tracking-preferences={%22version%22:1%2C%22destinations%22:{%22Actions%20Amplitude%22:false%2C%22AdWords%22:false%2C%22Google%20AdWords%20New%22:false%2C%22Google%20Enhanced%20Conversions%22:false%2C%22Google%20Tag%20Manager%22:false%2C%22Impact%20Partnership%20Cloud%22:false%2C%22Optimizely%22:false}%2C%22custom%22:{%22advertising%22:false%2C%22functional%22:false%2C%22marketingAndAnalytics%22:false}}; OAuthLoginSessionId=63b1a89d-1ac2-43f7-8e79-55a9ca5e761d; TCGAuthTicket_Production=F453BFF2FA3FAA3D1ACA23F319314F8273713DECEB06C69BB7C208A77C81559B46E1AA22A7E70FABC8D7F681A423C86870FAE318B76048CDE7BF6D73D220631B899BEBA86C422E1EBBF2ACD1921E0846F708AFE203C844031364E13B047465E7B41CB6460E4F4AAB278B614445B93E722E976688; BuyerRevalidationKey=; ASP.NET_SessionId=oouwzrh3jkhdrmaioooqhr4k; TCG_VisitorKey=431efcca-2d5b-404d-a04f-3ae979696051; __RequestVerificationToken_L2FkbWlu0=Lw1sfWh823UeJ7zRux0b1ZTI4Vg4i_dFt97a55aQpf-qBURVuwWDCJyuCxSwgLNLe9nPlfDSc1AMV5nyqhY4Q4jurxs1; fileDownloadToken=1740145585435; spDisabledUIFeatures=' \
-H 'origin: https://sellerportal.tcgplayer.com' \
-H 'priority: u=1, i' \
-H 'referer: https://sellerportal.tcgplayer.com/' \
-H 'sec-ch-ua: "Not(A:Brand";v="99", "Brave";v="133", "Chromium";v="133"' \
-H 'sec-ch-ua-mobile: ?0' \
-H 'sec-ch-ua-platform: "macOS"' \
-H 'sec-fetch-dest: empty' \
-H 'sec-fetch-mode: cors' \
-H 'sec-fetch-site: same-site' \
-H 'sec-gpc: 1' \
-H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36'
"""

View File

@ -18,4 +18,8 @@ curl -X POST "http://192.168.1.41:8000/api/boxes/d95d26a8-1f82-47f2-89fa-3f88a46
curl -X POST "http://192.168.1.41:8000/api/processOrders" \
-H "Content-Type: application/json" \
-d '{"order_ids": ["E576ED4C-A472E7-36237","E576ED4C-16EDF6-A340B","E576ED4C-A25B9C-A8855","E576ED4C-0E1A20-C6350","E576ED4C-9E8C21-3A249","E576ED4C-3825F0-5A5CC","E576ED4C-628925-6348B","E576ED4C-5F4314-6E3D2","E576ED4C-60E0B9-69D1D","E576ED4C-4BEC42-B2D0A","E576ED4C-5253F2-E2E16","E576ED4C-C08EA2-F51B4","E576ED4C-EE350E-BA82C","E576ED4C-CB067C-21150","E576ED4C-85DE3E-4E518","E576ED4C-27DB4A-A7729","E576ED4C-91A537-2AEBA","E576ED4C-3961D0-4F5A9","E576ED4C-EB7B7D-DBE0D","E576ED4C-1F9576-A9351","E576ED4C-7EBF1E-6FDB9","E576ED4C-F549E2-C558B","E576ED4C-215B45-4F177","E576ED4C-572FAA-004F7","E576ED4C-9D5F33-1A3C4","E576ED4C-87276B-63EC8","E576ED4C-2143E7-4DE1B","E576ED4C-41E56A-04D55","E576ED4C-789397-BF6AD","E576ED4C-2F3F46-154FE","E576ED4C-EFCBEE-3FE93","E576ED4C-3ADBAE-7CA1B","E576ED4C-D9F68F-A5E6F","E576ED4C-DEA6E2-8B590","E576ED4C-86D96B-DC5C4","E576ED4C-EDFABA-67C3C","E576ED4C-C57373-3F638","E576ED4C-B2C2B4-FF53B","E576ED4C-3788E5-B3653","E576ED4C-8A573A-BB51B","E576ED4C-497380-63F5C","E576ED4C-A6C3F2-C7BF2","E576ED4C-FAC80B-148F3","E576ED4C-ECF1F3-AF3A4"]}'
-d '{"order_ids": ["E576ED4C-AD8CC8-9E57E","E576ED4C-2EC48E-7F185","E576ED4C-9B72C2-5E208","E576ED4C-67DAA6-B06B5","E576ED4C-7E52E0-CCE11","E576ED4C-572216-87C96","E576ED4C-37BADB-8BE45","E576ED4C-2D9E83-89C64","E576ED4C-C8080B-8066C","E576ED4C-A41099-E4F92","E576ED4C-2B5122-5E719","E576ED4C-95BB1D-07DDA","E576ED4C-1CF99A-20072","E576ED4C-342542-28CDB","E576ED4C-42720B-514DB","E576ED4C-911CB9-15174","E576ED4C-EBD55A-27AE6","E576ED4C-CC32F2-76408","E576ED4C-45328B-E65B4","E576ED4C-1F26F5-84367","E576ED4C-1D4FE5-71100","E576ED4C-BCEC53-A7CF2","E576ED4C-132791-5AF2E"]}'
curl -X POST "http://192.168.1.41:8000/api/processOrders" \
-H "Content-Type: application/json" \
-d '{}'

View File

@ -2,41 +2,69 @@ alembic==1.14.1
annotated-types==0.7.0
anyio==4.8.0
APScheduler==3.11.0
attrs==25.3.0
blabel==0.1.6
brother_ql_next==0.11.3
Brotli==1.1.0
browser-cookie3==0.20.1
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
click==8.1.8
coverage==7.6.10
cssselect2==0.8.0
fastapi==0.115.8
fonttools==4.57.0
future==1.0.0
greenlet==3.1.1
h11==0.14.0
httpcore==1.0.7
httpx==0.28.1
idna==3.10
iniconfig==2.0.0
jeepney==0.9.0
Jinja2==3.1.6
jsons==1.6.3
lz4==4.4.3
Mako==1.3.9
MarkupSafe==3.0.2
numpy==2.2.2
packaging==24.2
packbits==0.6
pandas==2.2.3
pdf2image==1.17.0
pillow==11.1.0
pluggy==1.5.0
psycopg2-binary==2.9.10
pycparser==2.22
pycryptodomex==3.21.0
pydantic==2.10.6
pydantic_core==2.27.2
pydyf==0.11.0
pyphen==0.17.2
pyStrich==0.9
pytest==8.3.4
pytest-asyncio==0.25.3
pytest-cov==6.0.0
python-barcode==0.15.1
python-dateutil==2.9.0.post0
python-multipart==0.0.20
pytz==2025.1
pyusb==1.3.1
qrcode==8.1
requests==2.32.3
six==1.17.0
sniffio==1.3.1
SQLAlchemy==2.0.37
starlette==0.45.3
tinycss2==1.4.0
tinyhtml5==2.0.0
typing_extensions==4.12.2
typish==1.9.3
tzdata==2025.1
tzlocal==5.2
urllib3==2.3.0
uvicorn==0.34.0
weasyprint==65.0
webencodings==0.5.1
zopfli==0.2.3.post1