81 Commits

Author SHA1 Message Date
8f1789101c j
All checks were successful
Deploy App to Docker / deploy (push) Successful in 19s
2025-04-20 19:33:56 -04:00
976f2683a4 asdfsad
All checks were successful
Deploy App to Docker / deploy (push) Successful in 19s
2025-04-20 19:29:33 -04:00
b151ef9920 omg
All checks were successful
Deploy App to Docker / deploy (push) Successful in 33s
2025-04-20 19:26:48 -04:00
fb3134723a sadf
All checks were successful
Deploy App to Docker / deploy (push) Successful in 40s
2025-04-14 22:27:37 -04:00
8155ba2f8d a
All checks were successful
Deploy App to Docker / deploy (push) Successful in 27s
2025-04-14 21:37:11 -04:00
a97ba6e858 asdf
All checks were successful
Deploy App to Docker / deploy (push) Successful in 28s
2025-04-14 21:01:04 -04:00
3939c21a72 asdf
All checks were successful
Deploy App to Docker / deploy (push) Successful in 29s
2025-04-14 20:31:03 -04:00
9ff87dc107 asdf
All checks were successful
Deploy App to Docker / deploy (push) Successful in 44s
2025-04-14 19:18:49 -04:00
de3821aa80 asdf
All checks were successful
Deploy App to Docker / deploy (push) Successful in 30s
2025-04-12 14:33:03 -04:00
e67c1aa9f3 asdf
All checks were successful
Deploy App to Docker / deploy (push) Successful in 28s
2025-04-12 14:17:33 -04:00
94a3a517c7 asdf
All checks were successful
Deploy App to Docker / deploy (push) Successful in 29s
2025-04-12 14:01:57 -04:00
64897392f1 asdf
All checks were successful
Deploy App to Docker / deploy (push) Successful in 57s
2025-04-12 13:50:01 -04:00
3199a3259f asdf
All checks were successful
Deploy App to Docker / deploy (push) Successful in 33s
2025-04-12 13:27:43 -04:00
0e5ba991db FUCK
All checks were successful
Deploy App to Docker / deploy (push) Successful in 1m2s
2025-04-11 14:57:20 -04:00
3601bcc81b asdf
All checks were successful
Deploy App to Docker / deploy (push) Successful in 42s
2025-04-11 13:32:35 -04:00
c234285788 asdf
All checks were successful
Deploy App to Docker / deploy (push) Successful in 49s
2025-04-11 12:06:56 -04:00
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
544f789e2e aa 2025-04-04 16:02:11 -04:00
c1ab5d611f a 2025-04-04 15:33:47 -04:00
c9b57f00ea asdf 2025-04-04 15:31:06 -04:00
45c589d225 b 2025-04-04 15:10:39 -04:00
60eee07249 a 2025-04-04 14:54:51 -04:00
3d95553cbd a 2025-04-02 08:17:13 -04:00
2d01dae9ed a 2025-03-28 11:51:57 -04:00
1983f19fa7 a 2025-03-28 10:46:51 -04:00
8a26abc5f4 a 2025-03-24 15:31:38 -04:00
d76258eb55 asdf 2025-03-18 12:38:30 -04:00
86498d54b4 agg add file, skip foil api pricing, update pricing algo 2025-03-18 12:11:39 -04:00
2800135375 a 2025-02-28 18:41:35 -05:00
c3b4fe28d2 a 2025-02-28 18:38:50 -05:00
d4ffe180a3 a 2025-02-28 18:38:26 -05:00
8b0396ba00 a 2025-02-28 10:12:49 -05:00
99dfc3f6f8 asdf 2025-02-28 10:09:29 -05:00
8a4ec31bee 123 2025-02-28 10:04:24 -05:00
cafc77d07f asdf 2025-02-28 10:04:07 -05:00
dff5bc4a23 k 2025-02-27 13:29:41 -05:00
8097fba83c a 2025-02-27 12:43:05 -05:00
da492180b4 api pricing 2025-02-27 12:37:02 -05:00
e13b871fda asdf 2025-02-21 12:28:08 -05:00
ac6397de01 asdf 2025-02-21 12:26:28 -05:00
4c6d256316 order fix 2025-02-21 12:25:02 -05:00
1bf255d0fe orders 2025-02-21 11:46:50 -05:00
21 changed files with 1282 additions and 146 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

2
.gitignore vendored
View File

@@ -174,3 +174,5 @@ temp/
.DS_Store .DS_Store
*.db-journal *.db-journal
cookies/ cookies/
alembic/versions/*
*.csv

View File

@@ -4,6 +4,20 @@ WORKDIR /app
ENV DATABASE_URL=postgresql://poggers:giga!@192.168.1.41:5432/omegatcgdb 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 . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt

View File

@@ -1,2 +1,3 @@
# giga_tcg # 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

@@ -328,6 +328,73 @@ class TCGPlayerGroups(Base):
modified_on = Column(String) modified_on = Column(String)
category_id = Column(Integer) category_id = Column(Integer)
class Orders(Base):
__tablename__ = 'orders'
id = Column(String, primary_key=True)
order_id = Column(String, unique=True)
buyer_name = Column(String)
recipient_name = Column(String)
recipient_address_one = Column(String)
recipient_address_two = Column(String)
recipient_city = Column(String)
recipient_state = Column(String)
recipient_zip = Column(String)
recipient_country = Column(String)
order_date = Column(String)
status = Column(String)
num_products = Column(Integer)
num_cards = Column(Integer)
product_amount = Column(Float)
shipping_amount = Column(Float)
gross_amount = Column(Float)
fee_amount = Column(Float)
net_amount = Column(Float)
direct_fee_amount = Column(Float)
date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class OrderProducts(Base):
__tablename__ = 'order_products'
id = Column(String, primary_key=True)
order_id = Column(String, ForeignKey('orders.id'))
product_id = Column(String, ForeignKey('products.id'))
quantity = Column(Integer)
unit_price = Column(Float)
class APIPricing(Base):
__tablename__ = 'api_pricing'
id = Column(String, primary_key=True)
product_id = Column(String, ForeignKey('products.id'))
pricing_data = Column(String)
date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class TCGPlayerInventory(Base):
__tablename__ = 'tcgplayer_inventory'
id = Column(String, primary_key=True)
tcgplayer_id = Column(Integer)
product_line = Column(String)
set_name = Column(String)
product_name = Column(String)
title = Column(String)
number = Column(String)
rarity = Column(String)
condition = Column(String)
tcg_market_price = Column(Float)
tcg_direct_low = Column(Float)
tcg_low_price_with_shipping = Column(Float)
tcg_low_price = Column(Float)
total_quantity = Column(Integer)
add_to_quantity = Column(Integer)
tcg_marketplace_price = Column(Float)
photo_url = Column(String)
date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
# enums # enums
class RarityEnum(str, Enum): class RarityEnum(str, Enum):

View File

@@ -10,6 +10,7 @@ from app.services.product import ProductService
from app.services.inventory import InventoryService from app.services.inventory import InventoryService
from app.services.task import TaskService from app.services.task import TaskService
from app.services.storage import StorageService from app.services.storage import StorageService
from app.services.tcgplayer_api import TCGPlayerAPIService
from app.db.database import get_db from app.db.database import get_db
from app.schemas.file import CreateFileRequest from app.schemas.file import CreateFileRequest
from app.schemas.box import CreateBoxRequest, UpdateBoxRequest, CreateOpenBoxRequest from app.schemas.box import CreateBoxRequest, UpdateBoxRequest, CreateOpenBoxRequest
@@ -18,6 +19,10 @@ from app.schemas.box import CreateBoxRequest, UpdateBoxRequest, CreateOpenBoxReq
DB = Annotated[Session, Depends(get_db)] DB = Annotated[Session, Depends(get_db)]
# Base Services (no dependencies besides DB) # Base Services (no dependencies besides DB)
def get_tcgplayer_api_service(db: DB) -> TCGPlayerAPIService:
"""TCGPlayerAPIService with only database dependency"""
return TCGPlayerAPIService(db)
def get_file_service(db: DB) -> FileService: def get_file_service(db: DB) -> FileService:
"""FileService with only database dependency""" """FileService with only database dependency"""
return FileService(db) return FileService(db)
@@ -61,10 +66,11 @@ def get_box_service(
def get_task_service( def get_task_service(
db: DB, db: DB,
product_service: Annotated[ProductService, Depends(get_product_service)], product_service: Annotated[ProductService, Depends(get_product_service)],
pricing_service: Annotated[PricingService, Depends(get_pricing_service)] pricing_service: Annotated[PricingService, Depends(get_pricing_service)],
tcgplayer_api_service: Annotated[TCGPlayerAPIService, Depends(get_tcgplayer_api_service)]
) -> TaskService: ) -> TaskService:
"""TaskService depends on ProductService and TCGPlayerService""" """TaskService depends on ProductService and TCGPlayerService"""
return TaskService(db, product_service, pricing_service) return TaskService(db, product_service, pricing_service, tcgplayer_api_service)
# Form data dependencies # Form data dependencies
def get_create_file_metadata( def get_create_file_metadata(

View File

@@ -15,6 +15,7 @@ from app.dependencies import (
get_product_service, get_product_service,
get_storage_service, get_storage_service,
get_inventory_service, get_inventory_service,
get_tcgplayer_api_service
) )
logging.basicConfig( logging.basicConfig(
@@ -69,7 +70,8 @@ async def startup_event():
tcgplayer_service = get_tcgplayer_service(db, file_service) tcgplayer_service = get_tcgplayer_service(db, file_service)
pricing_service = get_pricing_service(db, file_service, tcgplayer_service) pricing_service = get_pricing_service(db, file_service, tcgplayer_service)
product_service = get_product_service(db, file_service, tcgplayer_service, storage_service) product_service = get_product_service(db, file_service, tcgplayer_service, storage_service)
task_service = get_task_service(db, product_service, pricing_service) tcgplayer_api_service = get_tcgplayer_api_service(db)
task_service = get_task_service(db, product_service, pricing_service, tcgplayer_api_service)
# Start task service # Start task service
await task_service.start() await task_service.start()

View File

@@ -25,10 +25,12 @@ from app.schemas.box import (
CreateOpenBoxResponse, CreateOpenBoxResponse,
OpenBoxSchema OpenBoxSchema
) )
from app.schemas.orders import ProcessOrdersResponse
from app.services.file import FileService from app.services.file import FileService
from app.services.box import BoxService from app.services.box import BoxService
from app.services.task import TaskService from app.services.task import TaskService
from app.services.pricing import PricingService from app.services.pricing import PricingService
from app.services.tcgplayer_api import TCGPlayerAPIService
from app.dependencies import ( from app.dependencies import (
get_file_service, get_file_service,
get_box_service, get_box_service,
@@ -37,7 +39,8 @@ from app.dependencies import (
get_box_data, get_box_data,
get_box_update_data, get_box_update_data,
get_open_box_data, get_open_box_data,
get_pricing_service get_pricing_service,
get_tcgplayer_api_service
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -313,3 +316,24 @@ async def update_cookies(
status_code=500, status_code=500,
detail=f"Failed to update cookies: {str(e)}" detail=f"Failed to update cookies: {str(e)}"
) )
class TCGPlayerOrderRequest(BaseModel):
# optional
order_ids: Optional[List[str]] = None
@router.post("/processOrders", response_model=ProcessOrdersResponse)
async def process_orders(
body: TCGPlayerOrderRequest,
tcgplayer_api_service: TCGPlayerAPIService = Depends(get_tcgplayer_api_service),
) -> ProcessOrdersResponse:
"""Process TCGPlayer orders."""
try:
orders = tcgplayer_api_service.process_open_orders(body.order_ids)
return ProcessOrdersResponse(
status_code=200,
success=True,
orders=orders
)
except Exception as e:
logger.error(f"Process orders failed: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))

View File

@@ -1,19 +0,0 @@
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from datetime import datetime
# FILE
class OrderSchema(BaseModel):
id: str = Field(..., title="id")
filename: str = Field(..., title="filename")
type: str = Field(..., title="type")
filesize_kb: float = Field(..., title="filesize_kb")
source: str = Field(..., title="source")
status: str = Field(..., title="status")
service: Optional[str] = Field(None, title="service")
date_created: datetime = Field(..., title="date_created")
date_modified: datetime = Field(..., title="date_modified")
# This enables ORM mode
model_config = ConfigDict(from_attributes=True)

9
app/schemas/orders.py Normal file
View File

@@ -0,0 +1,9 @@
from pydantic import BaseModel
class OrderSchema(BaseModel):
order_id: str
class ProcessOrdersResponse(BaseModel):
status_code: int
success: bool
orders: list[str]

View File

@@ -1,16 +1,21 @@
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.db.models import File, CardTCGPlayer, Price from app.db.models import File, CardTCGPlayer, Price, TCGPlayerInventory
from app.services.util._dataframe import TCGPlayerPricingRow, DataframeUtil from app.services.util._dataframe import TCGPlayerPricingRow, DataframeUtil
from app.services.file import FileService from app.services.file import FileService
from app.services.tcgplayer import TCGPlayerService from app.services.tcgplayer import TCGPlayerService
from uuid import uuid4 from uuid import uuid4
from app.db.utils import db_transaction from app.db.utils import db_transaction
from typing import List, Dict from typing import List, Dict
from decimal import Decimal, ROUND_HALF_UP
import pandas as pd import pandas as pd
import numpy as np
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
ACTIVE_PRICING_ALGORITHIM = 'tcgplayer_recommended_algo' # 'default_pricing_algo' or 'tcgplayer_recommended_algo'
FREE_SHIPPING = True
class PricingService: class PricingService:
def __init__(self, db: Session, file_service: FileService, tcgplayer_service: TCGPlayerService): def __init__(self, db: Session, file_service: FileService, tcgplayer_service: TCGPlayerService):
@@ -22,6 +27,8 @@ class PricingService:
# function for taking a tcgplayer pricing export with all set ids and loading it into the price table # function for taking a tcgplayer pricing export with all set ids and loading it into the price table
# can be run as needed or scheduled # can be run as needed or scheduled
def get_pricing_export_content(self, file: File = None) -> bytes: def get_pricing_export_content(self, file: File = None) -> bytes:
if ACTIVE_PRICING_ALGORITHIM == 'default_pricing_algo' and FREE_SHIPPING == True:
print("asdf")
if file: if file:
file_content = self.file_service.get_file_content(file.id) file_content = self.file_service.get_file_content(file.id)
else: else:
@@ -93,18 +100,22 @@ class PricingService:
def cron_load_prices(self, file: File = None): def cron_load_prices(self, file: File = None):
file_content = self.get_pricing_export_content(file) file_content = self.get_pricing_export_content(file)
self.tcgplayer_service.load_tcgplayer_cards(file_content)
self.load_pricing_csv_content_to_db(file_content) self.load_pricing_csv_content_to_db(file_content)
def get_all_prices_for_products(self, product_ids: List[str]) -> Dict[str, Dict[str, float]]: def get_latest_prices_for_products(self, product_ids: List[str]) -> Dict[str, Dict[str, float]]:
all_prices = self.db.query(Price).filter( latest_prices = self.db.query(Price).filter(
Price.product_id.in_(product_ids) Price.product_id.in_(product_ids)
).distinct(Price.product_id, Price.type).order_by(
Price.product_id,
Price.type,
Price.date_created.desc()
).all() ).all()
price_lookup = {} price_lookup = {}
for price in all_prices: for price in latest_prices:
if price.product_id not in price_lookup: price_lookup.setdefault(price.product_id, {})[price.type] = price.price
price_lookup[price.product_id] = {}
price_lookup[price.product_id][price.type] = price.price
return price_lookup return price_lookup
def apply_price_to_df_columns(self, row: pd.Series, price_lookup: Dict[str, Dict[str, float]]) -> pd.Series: def apply_price_to_df_columns(self, row: pd.Series, price_lookup: Dict[str, Dict[str, float]]) -> pd.Series:
@@ -113,42 +124,183 @@ class PricingService:
row[price_type] = price row[price_type] = price
return row return row
def default_pricing_algo(self, row: pd.Series) -> pd.Series: def smooth_markup(self, price, markup_bands):
"""Default pricing algorithm with complex pricing rules""" """
tcg_low = row.get('tcg_low_price') Applies a smoothed markup based on the given price and markup bands.
tcg_low_shipping = row.get('tcg_low_price_with_shipping') Uses numpy for smooth transitions.
tcg_market_price = row.get('tcg_market_price') """
# 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()])
if pd.isna(tcg_low) or pd.isna(tcg_low_shipping): # 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('tcg_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}") logger.warning(f"Missing pricing data for row: {row}")
row['new_price'] = None row['new_price'] = None
return row return row
# Apply pricing rules TWO_PLACES = Decimal('0.01')
if tcg_market_price < 1 and tcg_market_price > 0.25:
new_price = tcg_market_price * 1.05
elif tcg_market_price < 0.25:
new_price = 0.25
elif tcg_low < 5 or tcg_low_shipping < 5:
new_price = round(tcg_low+((abs(tcg_market_price-tcg_low))*.75), 2)
elif tcg_low_shipping > 20:
new_price = round(tcg_low_shipping * 1.0125, 2)
else:
# new_price = round(tcg_low_shipping * 1.08, 2)
new_price = round(tcg_market_price * 1.03)
# if new price is less than half of market price, set to 90% market
if new_price < (tcg_market_price / 2):
new_price = round(tcg_market_price * 0.85, 2)
if new_price < 0.25:
new_price = 0.25
row['new_price'] = new_price # Original markup bands
markup_bands = {
Decimal('2.34'): (Decimal('0.01'), Decimal('0.50')),
Decimal('1.36'): (Decimal('0.51'), Decimal('1.00')),
Decimal('1.24'): (Decimal('1.01'), Decimal('3.00')),
Decimal('1.15'): (Decimal('3.01'), Decimal('20.00')),
Decimal('1.06'): (Decimal('20.01'), Decimal('35.00')),
Decimal('1.05'): (Decimal('35.01'), Decimal('50.00')),
Decimal('1.03'): (Decimal('50.01'), Decimal('100.00')),
Decimal('1.02'): (Decimal('100.01'), Decimal('200.00')),
Decimal('1.01'): (Decimal('200.01'), Decimal('1000.00'))
}
# Adjust markups if quantity is high
if quantity > 3:
adjusted_bands = {}
increment = Decimal('0.20')
for markup, price_range in zip(markup_bands.keys(), markup_bands.values()):
new_markup = Decimal(str(markup)) + increment
adjusted_bands[new_markup] = price_range
increment -= Decimal('0.02')
markup_bands = adjusted_bands
#if FREE_SHIPPING:
#if tcg_low_shipping and (tcg_low >= Decimal('5.00')):
#tcg_compare_price = tcg_low_shipping
#elif tcg_low_shipping and (tcg_low < Decimal('5.00')):
#tcg_compare_price = max(tcg_low_shipping - Decimal('1.31'), tcg_low)
#elif tcg_low:
#tcg_compare_price = tcg_low
#else:
#logger.warning(f"No TCG low or shipping price available for row: {row}")
#row['new_price'] = None
#return row
#else:
#tcg_compare_price = tcg_low
#if tcg_compare_price is None:
#logger.warning(f"No TCG low price available for row: {row}")
#row['new_price'] = None
#return row
tcg_compare_price = tcg_low
# Apply the smoothed markup
new_price = self.smooth_markup(tcg_compare_price, markup_bands)
# Enforce minimum price
if new_price < Decimal('0.35'):
new_price = Decimal('0.25')
# Avoid huge price drops
#if current_price is not None and Decimal(str(((current_price - new_price) / current_price))) > Decimal('0.5'):
#logger.warning(f"Price drop too large for row: {row}")
#new_price = current_price
# Round to 2 decimal places
new_price = new_price.quantize(TWO_PLACES, rounding=ROUND_HALF_UP)
# Convert back to float for 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"""
# 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
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
if tcg_market_price < Decimal('1') and tcg_market_price > Decimal('0.25'):
new_price = tcg_market_price * Decimal('1.25')
elif tcg_market_price < Decimal('0.25'):
new_price = Decimal('0.25')
elif tcg_market_price < Decimal('5'):
new_price = tcg_market_price * Decimal('1.08')
elif tcg_market_price < Decimal('10'):
new_price = tcg_market_price * Decimal('1.06')
elif tcg_market_price < Decimal('20'):
new_price = tcg_market_price * Decimal('1.0125')
elif tcg_market_price < Decimal('50'):
new_price = tcg_market_price * Decimal('0.99')
elif tcg_market_price < Decimal('100'):
new_price = tcg_market_price * Decimal('0.98')
else:
new_price = tcg_market_price * Decimal('1.09')
if new_price < Decimal('0.25'):
new_price = Decimal('0.25')
if quantity > 3:
new_price = new_price * Decimal('1.1')
# 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 return row
def apply_pricing_algo(self, row: pd.Series, pricing_algo: callable = None) -> pd.Series: def apply_pricing_algo(self, row: pd.Series, pricing_algo: callable = None) -> pd.Series:
"""Modified to handle the pricing algorithm as an instance method""" """Modified to handle the pricing algorithm as an instance method"""
if pricing_algo is None: if pricing_algo:
logger.debug(f"Using custom pricing algorithm: {pricing_algo.__name__}")
return pricing_algo(row)
elif ACTIVE_PRICING_ALGORITHIM == 'default_pricing_algo':
logger.debug(f"Using default pricing algorithm: {self.default_pricing_algo.__name__}")
pricing_algo = self.default_pricing_algo pricing_algo = self.default_pricing_algo
elif ACTIVE_PRICING_ALGORITHIM == 'tcgplayer_recommended_algo':
logger.debug(f"Using TCGPlayer recommended algorithm: {self.tcgplayer_recommended_algo.__name__}")
pricing_algo = self.tcgplayer_recommended_algo
else:
logger.debug(f"Using default pricing algorithm: {self.default_pricing_algo.__name__}")
pricing_algo = self.default_pricing_algo
return pricing_algo(row) return pricing_algo(row)
def generate_tcgplayer_inventory_update_file_with_pricing(self, open_box_ids: List[str] = None) -> bytes: def generate_tcgplayer_inventory_update_file_with_pricing(self, open_box_ids: List[str] = None) -> bytes:
@@ -177,15 +329,24 @@ class PricingService:
.filter(CardTCGPlayer.tcgplayer_id.in_(tcgplayer_ids)) .filter(CardTCGPlayer.tcgplayer_id.in_(tcgplayer_ids))
.all() .all()
} }
# set listed price
df['listed_price'] = df['tcg_marketplace_price'].copy()
# Map the ids using the dictionary # Map tcgplayer_id to product_id, ensure strings, keep None if not found
df['product_id'] = df['tcgplayer_id'].map(product_id_mapping) df['product_id'] = df['tcgplayer_id'].map(product_id_mapping).apply(lambda x: str(x) if pd.notnull(x) else None)
price_lookup = self.get_all_prices_for_products(df['product_id'].unique()) # 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_latest_prices_for_products(df['product_id'].unique())
# Apply price columns # Apply price columns
df = df.apply(lambda row: self.apply_price_to_df_columns(row, price_lookup), axis=1) df = df.apply(lambda row: self.apply_price_to_df_columns(row, price_lookup), axis=1)
logger.debug(f"Applying pricing algorithm: {ACTIVE_PRICING_ALGORITHIM}")
# Apply pricing algorithm # Apply pricing algorithm
df = df.apply(self.apply_pricing_algo, axis=1) df = df.apply(self.apply_pricing_algo, axis=1)
@@ -193,9 +354,12 @@ class PricingService:
if update_type == 'update': if update_type == 'update':
df = df[df['new_price'] != df['listed_price']] df = df[df['new_price'] != df['listed_price']]
# Set marketplace price # Set marketplace price
df['TCG Marketplace Price'] = df['new_price'] df['TCG Marketplace Price'] = df['new_price']
df['Title'] = ''
column_mapping = { column_mapping = {
'tcgplayer_id': 'TCGplayer Id', 'tcgplayer_id': 'TCGplayer Id',
'product_line': 'Product Line', 'product_line': 'Product Line',
@@ -218,6 +382,19 @@ class PricingService:
# Now do your column selection # Now do your column selection
df = df[desired_columns] df = df[desired_columns]
if update_type == 'update':
with db_transaction(self.db):
self.db.query(TCGPlayerInventory).delete()
self.db.flush()
# copy df to modify before inserting
df_copy = df.copy()
df_copy['id'] = df_copy.apply(lambda x: str(uuid4()), axis=1)
# rename columns lowercase no space
df_copy.columns = df_copy.columns.str.lower().str.replace(' ', '_')
for index, row in df_copy.iterrows():
tcgplayer_inventory = TCGPlayerInventory(**row.to_dict())
self.db.add(tcgplayer_inventory)
# remove any rows with no price # remove any rows with no price
#df = df[df['TCG Marketplace Price'] != 0] #df = df[df['TCG Marketplace Price'] != 0]
#df = df[df['TCG Marketplace Price'].notna()] #df = df[df['TCG Marketplace Price'].notna()]

View File

@@ -10,7 +10,7 @@ import io
# Printer settings # Printer settings
printer_model = "QL-1100" printer_model = "QL-1100"
backend = 'pyusb' # Changed from network to USB backend = 'pyusb'
printer = 'usb://0x04f9:0x20a7' printer = 'usb://0x04f9:0x20a7'
def convert_pdf_to_image(pdf_path): def convert_pdf_to_image(pdf_path):
@@ -45,6 +45,8 @@ def create_address_label(input_data, font_size=30, is_pdf=False):
font = ImageFont.truetype("C:\\Windows\\Fonts\\arial.ttf", size=font_size) font = ImageFont.truetype("C:\\Windows\\Fonts\\arial.ttf", size=font_size)
elif platform.system() == 'Darwin': elif platform.system() == 'Darwin':
font = ImageFont.truetype("/Library/Fonts/Arial.ttf", size=font_size) font = ImageFont.truetype("/Library/Fonts/Arial.ttf", size=font_size)
elif platform.system() == 'Linux':
font = ImageFont.truetype("/usr/share/fonts/truetype/msttcorefonts/arial.ttf", size=font_size)
margin = 20 margin = 20
lines = input_data.split('\n') lines = input_data.split('\n')
@@ -71,10 +73,8 @@ def print_address_label(input_data, font_size=30, is_pdf=False, label_size='29x9
if not image: if not image:
raise Exception("Failed to create label image") raise Exception("Failed to create label image")
# For 4x6 shipping labels from Pirate Ship
if label_size == '4x6': if label_size == '4x6':
# Resize image to fit 4x6 format if needed target_width = 1164
target_width = 1164 # Adjusted for 4x6 format
target_height = 1660 target_height = 1660
image = image.resize((target_width, target_height), Image.LANCZOS) image = image.resize((target_width, target_height), Image.LANCZOS)
@@ -92,7 +92,6 @@ def print_address_label(input_data, font_size=30, is_pdf=False, label_size='29x9
red=False, red=False,
dpi_600=False, dpi_600=False,
hq=True, hq=True,
#cut=True
cut=False cut=False
) )
@@ -135,33 +134,23 @@ def process_tcg_shipping_export(file_path, require_input=False, font_size=60, pr
else: else:
sleep(1) sleep(1)
# Example usage
if __name__ == "__main__": if __name__ == "__main__":
# Example for regular address label
address = """John Doe
123 Main Street
Apt 4B
City, State 12345"""
# Example for TCG Player export
shipping_export_file = "_TCGplayer_ShippingExport_20250201_115949.csv"
# Example for Pirate Ship PDF
pirate_ship_pdf = "C:\\Users\\joshu\\Downloads\\2025-02-10---greg-creek---9400136208070411592215.pdf"
# Choose which type to process # Choose which type to process
label_type = input("Enter label type (1 for regular, 2 for TCG, 3 for Pirate Ship): ") label_type = input("Enter label type (1 for regular, 2 for TCG, 3 for Pirate Ship): ")
if label_type == "1": if label_type == "1":
address = input("Enter the address to print: ")
preview_label(address, font_size=60) preview_label(address, font_size=60)
user_input = input("Press 'p' to print the label or any other key to cancel: ") user_input = input("Press 'p' to print the label or any other key to cancel: ")
if user_input.lower() == 'p': if user_input.lower() == 'p':
print_address_label(address, font_size=60) print_address_label(address, font_size=60)
elif label_type == "2": elif label_type == "2":
shipping_export_file = input("Enter the path to the TCG Player shipping export CSV file: ")
process_tcg_shipping_export(shipping_export_file, font_size=60, preview=False) process_tcg_shipping_export(shipping_export_file, font_size=60, preview=False)
elif label_type == "3": elif label_type == "3":
pirate_ship_pdf = input("Enter the path to the Pirate Ship PDF file: ")
process_pirate_ship_pdf(pirate_ship_pdf, preview=True) process_pirate_ship_pdf(pirate_ship_pdf, preview=True)
user_input = input("Press 'p' to print the label or any other key to cancel: ") user_input = input("Press 'p' to print the label or any other key to cancel: ")
if user_input.lower() == 'p': if user_input.lower() == 'p':

View File

@@ -5,16 +5,18 @@ from sqlalchemy.orm import Session
from app.services.product import ProductService from app.services.product import ProductService
from app.db.models import File from app.db.models import File
from app.services.pricing import PricingService from app.services.pricing import PricingService
from app.services.tcgplayer_api import TCGPlayerAPIService
class TaskService: class TaskService:
def __init__(self, db: Session, product_service: ProductService, pricing_service: PricingService): def __init__(self, db: Session, product_service: ProductService, pricing_service: PricingService, tcgplayer_api_service: TCGPlayerAPIService):
self.scheduler = BackgroundScheduler() self.scheduler = BackgroundScheduler()
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.tasks: Dict[str, Callable] = {} self.tasks: Dict[str, Callable] = {}
self.db = db self.db = db
self.product_service = product_service self.product_service = product_service
self.pricing_service = pricing_service self.pricing_service = pricing_service
self.tcgplayer_api_service = tcgplayer_api_service
async def start(self): async def start(self):
self.scheduler.start() self.scheduler.start()
@@ -23,7 +25,10 @@ class TaskService:
# self.pricing_service.generate_tcgplayer_inventory_update_file_with_pricing(['e20cc342-23cb-4593-89cb-56a0cb3ed3f3']) # self.pricing_service.generate_tcgplayer_inventory_update_file_with_pricing(['e20cc342-23cb-4593-89cb-56a0cb3ed3f3'])
def register_scheduled_tasks(self): def register_scheduled_tasks(self):
self.scheduler.add_job(self.hourly_pricing, 'cron', minute='45') 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.") self.logger.info("Scheduled tasks registered.")
def hourly_pricing(self): def hourly_pricing(self):
@@ -31,6 +36,14 @@ class TaskService:
self.pricing_service.cron_load_prices() self.pricing_service.cron_load_prices()
self.logger.info("Finished hourly pricing task") 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()
async def process_manabox_file(self, file: File): async def process_manabox_file(self, file: File):
self.logger.info("Processing ManaBox file") self.logger.info("Processing ManaBox file")
self.product_service.bg_process_manabox_file(file.id) self.product_service.bg_process_manabox_file(file.id)

View File

@@ -21,6 +21,8 @@ import pandas as pd
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from app.schemas.file import CreateFileRequest from app.schemas.file import CreateFileRequest
import os import os
from app.services.util._docker import DockerUtil
from sqlalchemy import func
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -53,6 +55,7 @@ class TCGPlayerService:
self.previous_request_time = None self.previous_request_time = None
self.df_util = DataframeUtil() self.df_util = DataframeUtil()
self.file_service = file_service self.file_service = file_service
self.docker_util = DockerUtil()
def _insert_groups(self, groups): def _insert_groups(self, groups):
for group in groups: for group in groups:
@@ -119,47 +122,6 @@ class TCGPlayerService:
logger.error(f"Failed to get browser cookies: {str(e)}") logger.error(f"Failed to get browser cookies: {str(e)}")
return None return None
def is_in_docker(self) -> bool:
"""Check if we're running inside a Docker container using multiple methods"""
# Method 1: Check cgroup
try:
with open('/proc/1/cgroup', 'r') as f:
content = f.read().lower()
if any(container_id in content for container_id in ['docker', 'containerd', 'kubepods']):
logger.debug("Docker detected via cgroup")
return True
except Exception as e:
logger.debug(f"Could not read cgroup file: {e}")
# Method 2: Check /.dockerenv file
if os.path.exists('/.dockerenv'):
logger.debug("Docker detected via /.dockerenv file")
return True
# Method 3: Check environment variables
docker_env = any(os.environ.get(var, False) for var in [
'DOCKER_CONTAINER',
'IN_DOCKER',
'KUBERNETES_SERVICE_HOST', # For k8s
'DOCKER_HOST'
])
if docker_env:
logger.debug("Docker detected via environment variables")
return True
# Method 4: Check container runtime
try:
with open('/proc/self/mountinfo', 'r') as f:
content = f.read().lower()
if any(rt in content for rt in ['docker', 'containerd', 'kubernetes']):
logger.debug("Docker detected via mountinfo")
return True
except Exception as e:
logger.debug(f"Could not read mountinfo: {e}")
logger.debug("No Docker environment detected")
return False
def _send_request(self, url: str, method: str, data=None, except_302=False) -> requests.Response: def _send_request(self, url: str, method: str, data=None, except_302=False) -> requests.Response:
"""Send a request with the specified cookies""" """Send a request with the specified cookies"""
# Rate limiting logic # Rate limiting logic
@@ -173,7 +135,7 @@ class TCGPlayerService:
# Move cookie initialization outside and make it more explicit # Move cookie initialization outside and make it more explicit
if not self.cookies: if not self.cookies:
if self.is_in_docker(): if self.docker_util.is_in_docker():
logger.debug("Running in Docker - using cookies from file") logger.debug("Running in Docker - using cookies from file")
self.cookies = self.get_cookies_from_file() self.cookies = self.get_cookies_from_file()
else: else:
@@ -537,34 +499,49 @@ class TCGPlayerService:
except SQLAlchemyError as e: except SQLAlchemyError as e:
raise RuntimeError(f"Failed to retrieve group IDs: {str(e)}") raise RuntimeError(f"Failed to retrieve group IDs: {str(e)}")
def load_tcgplayer_cards(self) -> File: def load_tcgplayer_cards(self, file_content):
try: try:
# Get pricing export
export_csv_file = self.get_pricing_export_for_all_products()
export_csv = self.file_service.get_file_content(export_csv_file.id)
# load to card tcgplayer # load to card tcgplayer
self.load_export_csv_to_card_tcgplayer(export_csv, export_csv_file.id) self.load_export_csv_to_card_tcgplayer(file_content)
return export_csv_file
except Exception as e: except Exception as e:
logger.error(f"Failed to load prices: {e}") logger.error(f"Failed to load prices: {e}")
raise raise
def open_box_cards_to_tcgplayer_inventory_df(self, open_box_ids: List[str]) -> pd.DataFrame: def open_box_cards_to_tcgplayer_inventory_df(self, open_box_ids: List[str]) -> pd.DataFrame:
tcgcards = (self.db.query(OpenBoxCard, CardTCGPlayer) # Using sqlalchemy to group and sum quantities for duplicate TCGplayer IDs
.filter(OpenBoxCard.open_box_id.in_(open_box_ids)) tcgcards = (self.db.query(
.join(CardTCGPlayer, OpenBoxCard.card_id == CardTCGPlayer.product_id) CardTCGPlayer.product_id,
.all()) CardTCGPlayer.tcgplayer_id,
CardTCGPlayer.product_line,
CardTCGPlayer.set_name,
CardTCGPlayer.product_name,
CardTCGPlayer.title,
CardTCGPlayer.number,
CardTCGPlayer.rarity,
CardTCGPlayer.condition,
func.sum(OpenBoxCard.quantity).label('quantity')
)
.filter(OpenBoxCard.open_box_id.in_(open_box_ids))
.join(CardTCGPlayer, OpenBoxCard.card_id == CardTCGPlayer.product_id)
.group_by(
CardTCGPlayer.tcgplayer_id,
CardTCGPlayer.product_id,
CardTCGPlayer.product_line,
CardTCGPlayer.set_name,
CardTCGPlayer.product_name,
CardTCGPlayer.title,
CardTCGPlayer.number,
CardTCGPlayer.rarity,
CardTCGPlayer.condition
)
.all())
if not tcgcards: if not tcgcards:
return None return None
# Create dataframe # Create dataframe directly from the query results
df = pd.DataFrame([(tcg.product_id, tcg.tcgplayer_id, tcg.product_line, tcg.set_name, tcg.product_name, df = pd.DataFrame(tcgcards,
tcg.title, tcg.number, tcg.rarity, tcg.condition, obc.quantity)
for obc, tcg in tcgcards],
columns=['product_id', 'tcgplayer_id', 'product_line', 'set_name', 'product_name', columns=['product_id', 'tcgplayer_id', 'product_line', 'set_name', 'product_name',
'title', 'number', 'rarity', 'condition', 'quantity']) 'title', 'number', 'rarity', 'condition', 'quantity'])

View File

@@ -0,0 +1,521 @@
from dataclasses import dataclass
import logging
from app.db.models import Orders, OrderProducts, CardTCGPlayer, CardManabox, APIPricing, TCGPlayerInventory
from app.services.util._requests import RequestsUtil
from app.services.util._docker import DockerUtil
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__)
@dataclass
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):
self.db = db
self.docker_util = DockerUtil()
self.requests_util = RequestsUtil()
self.is_in_docker = self.docker_util.is_in_docker()
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:
return self.requests_util.get_tcgplayer_cookies_from_file()
else:
return self.requests_util.get_tcgplayer_browser_cookies()
def get_order(self, order_id: str) -> dict:
url = f"{self.config.ORDER_BASE_URL}/{order_id}{self.config.API_VERSION}"
response = self.requests_util.send_request(url, method='GET', cookies=self.cookies)
if response:
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
sku_ids = [int(sku_id) for sku_id in sku_ids]
tcg_cards = self.db.query(CardTCGPlayer).filter(CardTCGPlayer.tcgplayer_id.in_(sku_ids)).all()
return {str(card.tcgplayer_id): card.product_id for card in tcg_cards}
def save_order(self, order: dict):
# check if order exists by order number
order_number = order['orderNumber']
existing_order = self.db.query(Orders).filter(Orders.order_id == order_number).first()
if existing_order:
logger.info(f"Order {order_number} already exists in database")
return existing_order
transaction = order['transaction']
shipping = order['shippingAddress']
products = order['products']
with db_transaction(self.db):
db_order = Orders(
id = str(uuid()),
order_id=order_number,
buyer_name=order['buyerName'],
recipient_name=shipping['recipientName'],
recipient_address_one=shipping['addressOne'],
recipient_address_two=shipping['addressTwo'] if 'addressTwo' in shipping else '',
recipient_city=shipping['city'],
recipient_state=shipping['territory'],
recipient_zip=shipping['postalCode'],
recipient_country=shipping['country'],
order_date=order['createdAt'],
status=order['status'],
num_products=len(products),
num_cards=sum([product['quantity'] for product in products]),
product_amount=transaction['productAmount'],
shipping_amount=transaction['shippingAmount'],
gross_amount=transaction['grossAmount'],
fee_amount=transaction['feeAmount'],
net_amount=transaction['netAmount'],
direct_fee_amount=transaction['directFeeAmount']
)
self.db.add(db_order)
self.db.flush()
product_ids = [product['skuId'] for product in products]
sku_to_product_id_mapping = self.get_product_ids_from_sku(product_ids)
order_products = []
for product in products:
product_id = sku_to_product_id_mapping.get(product['skuId'])
if product_id:
order_products.append(
OrderProducts(
id=str(uuid()),
order_id=db_order.id,
product_id=product_id,
quantity=product['quantity'],
unit_price=product['unitPrice']
)
)
self.db.add_all(order_products)
return db_order
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"
response = self.requests_util.bare_request(url, method='GET')
return response
def get_tcgplayer_pricing_data(self, tcgplayer_id: str):
if not self.session:
self.session = self.requests_util.get_session()
response = self.session.get("https://tcgplayer.com")
headers = {
'accept': 'application/json, text/plain, */*',
'accept-language': 'en-US,en;q=0.8',
'priority': 'u=1, i',
'sec-ch-ua': '"Not(A:Brand";v="99", "Brave";v="133", "Chromium";v="133"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-site',
'sec-gpc': '1',
'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"
response = self.session.get(url, headers=headers)
self.requests_util.previous_request_time = datetime.now()
return response
# pricing
def get_tcgplayer_pricing_data_for_product(self, product_id: str):
# get tcgplayer pricing data for a single card by product id
# product_id to manabox card
manabox_card = self.db.query(CardManabox).filter(CardManabox.product_id == product_id).first()
tcgplayer_card = self.db.query(CardTCGPlayer).filter(CardTCGPlayer.product_id == product_id).first()
if not manabox_card or not tcgplayer_card:
logger.warning(f"Card with product id {product_id} missing in either Manabox or TCGPlayer")
return None
mbfoil = manabox_card.foil
if str.lower(mbfoil) == 'foil':
logger.warning(f"Card with product id {product_id} is foil, skipping")
return None
# get scryfall id, tcgplayer id, and tcgplayer sku
scryfall_id = manabox_card.scryfall_id
tcgplayer_sku = tcgplayer_card.tcgplayer_id
tcgplayer_id = self.get_scryfall_data(scryfall_id).json().get('tcgplayer_id')
tcgplayer_pricing = self.get_tcgplayer_pricing_data(tcgplayer_id)
if not tcgplayer_pricing:
logger.warning(f"TCGPlayer pricing data not found for product id {product_id}")
return None
else:
logger.info(f"TCGPlayer pricing data found for product id {product_id}")
return tcgplayer_pricing.json()
def save_tcgplayer_pricing_data(self, product_id: str, pricing_data: dict):
# convert to json
pricing_data_json = json.dumps(pricing_data)
with db_transaction(self.db):
pricing_record = APIPricing(
id=str(uuid()),
product_id=product_id,
pricing_data=str(pricing_data_json)
)
self.db.add(pricing_record)
def cron_tcgplayer_api_pricing(self):
# Join both tables but retrieve both objects
results = self.db.query(TCGPlayerInventory, CardTCGPlayer).join(
CardTCGPlayer,
TCGPlayerInventory.tcgplayer_id == CardTCGPlayer.tcgplayer_id
).all()
for inventory, card in results:
# Now use card.product_id (from CardTCGPlayer)
pricing_data = self.get_tcgplayer_pricing_data_for_product(card.product_id)
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
# what does score mean? - totally ignore score, it seems related to price and changes based on what is on the page. probably some psy op shit to get you to buy expensive stuff, not useful for us
# can i get volatility from here?
# no historical data here
"""
curl 'https://mp-search-api.tcgplayer.com/v2/product/615745/details?mpfev=3279' \
-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; OAuthLoginSessionId=63b1a89d-1ac2-43f7-8e79-55a9ca5e761d; __RequestVerificationToken_L2FkbWlu0=Lw1sfWh823UeJ7zRux0b1ZTI4Vg4i_dFt97a55aQpf-qBURVuwWDCJyuCxSwgLNLe9nPlfDSc1AMV5nyqhY4Q4jurxs1; spDisabledUIFeatures=orders; SellerProximity=ZipCode=&MaxSellerDistance=1000&IsActive=false; tcg-uuid=613192dc-ecf6-481a-bec1-afdee8686db7; LastSeller=e576ed4c; __RequestVerificationToken=VFv72VLK6McJVzthg8O-41p7BNkdoW2jQAlDAu-ylO39qfzCddRi2-7bWiH4qloc8Vo_ZftOAAa5OhXL3OByFHIdlwY1; TCGAuthTicket_Production=270B0566400C905C51DEAD644E3CDBD634ECBCCC796B1F77717F461EE104FCE101CFAD2A8458319330A0931018A99214D4EA5601E7551E25E2069ACA550BB71775C0A04F30724E2C4E262CB167EAC2C2EB05D15F9EA08363FC6455B94654F1F110CF079E24201C3B8CEF26762423D8CAA71DDF7B; ASP.NET_SessionId=5ycv15jf0mon3l5adodmkog5; StoreSaveForLater_PRODUCTION=SFLK=a167bf88521f4d0fbeb7497a7ed74629&Ignore=false; TCG_VisitorKey=81fe992f-9a12-4926-a417-7815c4f94edd; setting=CD=US&M=1; SearchSortSettings=M=1&ProductSortOption=MinPrice&ProductSortDesc=True&PriceSortOption=Shipping&ProductResultDisplay=grid; tcg_analytics_previousPageData=%7B%22title%22%3A%22Seller%20Feedback%22%2C%22href%22%3A%22https%3A%2F%2Fshop.tcgplayer.com%2Fsellerfeedback%2Fbe27fef9%22%7D; fileDownloadToken=1740499419709; StoreCart_PRODUCTION=CK=b4f8aff616974a12a6b2811129b81ee2&Ignore=false; 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}}; tcg-segment-session=1740595460137%257C1740595481177' \
-H 'origin: https://www.tcgplayer.com' \
-H 'priority: u=1, i' \
-H 'referer: https://www.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'
"""
# get volatility also
"""
curl 'https://mpgateway.tcgplayer.com/v1/pricepoints/marketprice/skus/8395586/volatility?mpfev=3279' \
-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; OAuthLoginSessionId=63b1a89d-1ac2-43f7-8e79-55a9ca5e761d; __RequestVerificationToken_L2FkbWlu0=Lw1sfWh823UeJ7zRux0b1ZTI4Vg4i_dFt97a55aQpf-qBURVuwWDCJyuCxSwgLNLe9nPlfDSc1AMV5nyqhY4Q4jurxs1; spDisabledUIFeatures=orders; SellerProximity=ZipCode=&MaxSellerDistance=1000&IsActive=false; tcg-uuid=613192dc-ecf6-481a-bec1-afdee8686db7; LastSeller=e576ed4c; __RequestVerificationToken=VFv72VLK6McJVzthg8O-41p7BNkdoW2jQAlDAu-ylO39qfzCddRi2-7bWiH4qloc8Vo_ZftOAAa5OhXL3OByFHIdlwY1; TCGAuthTicket_Production=270B0566400C905C51DEAD644E3CDBD634ECBCCC796B1F77717F461EE104FCE101CFAD2A8458319330A0931018A99214D4EA5601E7551E25E2069ACA550BB71775C0A04F30724E2C4E262CB167EAC2C2EB05D15F9EA08363FC6455B94654F1F110CF079E24201C3B8CEF26762423D8CAA71DDF7B; ASP.NET_SessionId=5ycv15jf0mon3l5adodmkog5; StoreSaveForLater_PRODUCTION=SFLK=a167bf88521f4d0fbeb7497a7ed74629&Ignore=false; TCG_VisitorKey=81fe992f-9a12-4926-a417-7815c4f94edd; setting=CD=US&M=1; tcg_analytics_previousPageData=%7B%22title%22%3A%22Seller%20Feedback%22%2C%22href%22%3A%22https%3A%2F%2Fshop.tcgplayer.com%2Fsellerfeedback%2Fbe27fef9%22%7D; fileDownloadToken=1740499419709; StoreCart_PRODUCTION=CK=b4f8aff616974a12a6b2811129b81ee2&Ignore=false; 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}}; SearchCriteria=M=1&WantVerifiedSellers=False&WantDirect=False&WantSellersInCart=False; SearchSortSettings=M=1&ProductSortOption=Sales&ProductSortDesc=False&PriceSortOption=Shipping&ProductResultDisplay=grid; tcg-segment-session=1740597680921%257C1740598418227' \
-H 'origin: https://www.tcgplayer.com' \
-H 'priority: u=1, i' \
-H 'referer: https://www.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'
"""
# handle historical data later
# detailed range quarter - detailed pricing info for the last quarter. seems simple
"""
"""
# listings - lots of stuff here
"""
QUANTITY OVERVIEW
curl 'https://mp-search-api.tcgplayer.com/v1/product/615745/listings?mpfev=3279' \
-H 'accept: application/json, text/plain, */*' \
-H 'accept-language: en-US,en;q=0.8' \
-H 'content-type: application/json' \
-b 'tcgpartner=PK=TRADECARDS&M=1; valid=set=true; product-display-settings=sort=price+shipping&size=10; OAuthLoginSessionId=63b1a89d-1ac2-43f7-8e79-55a9ca5e761d; __RequestVerificationToken_L2FkbWlu0=Lw1sfWh823UeJ7zRux0b1ZTI4Vg4i_dFt97a55aQpf-qBURVuwWDCJyuCxSwgLNLe9nPlfDSc1AMV5nyqhY4Q4jurxs1; spDisabledUIFeatures=orders; SellerProximity=ZipCode=&MaxSellerDistance=1000&IsActive=false; tcg-uuid=613192dc-ecf6-481a-bec1-afdee8686db7; LastSeller=e576ed4c; __RequestVerificationToken=VFv72VLK6McJVzthg8O-41p7BNkdoW2jQAlDAu-ylO39qfzCddRi2-7bWiH4qloc8Vo_ZftOAAa5OhXL3OByFHIdlwY1; TCGAuthTicket_Production=270B0566400C905C51DEAD644E3CDBD634ECBCCC796B1F77717F461EE104FCE101CFAD2A8458319330A0931018A99214D4EA5601E7551E25E2069ACA550BB71775C0A04F30724E2C4E262CB167EAC2C2EB05D15F9EA08363FC6455B94654F1F110CF079E24201C3B8CEF26762423D8CAA71DDF7B; ASP.NET_SessionId=5ycv15jf0mon3l5adodmkog5; StoreSaveForLater_PRODUCTION=SFLK=a167bf88521f4d0fbeb7497a7ed74629&Ignore=false; TCG_VisitorKey=81fe992f-9a12-4926-a417-7815c4f94edd; setting=CD=US&M=1; SearchSortSettings=M=1&ProductSortOption=MinPrice&ProductSortDesc=True&PriceSortOption=Shipping&ProductResultDisplay=grid; tcg_analytics_previousPageData=%7B%22title%22%3A%22Seller%20Feedback%22%2C%22href%22%3A%22https%3A%2F%2Fshop.tcgplayer.com%2Fsellerfeedback%2Fbe27fef9%22%7D; fileDownloadToken=1740499419709; StoreCart_PRODUCTION=CK=b4f8aff616974a12a6b2811129b81ee2&Ignore=false; 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}}; tcg-segment-session=1740595460137%257C1740595481430' \
-H 'origin: https://www.tcgplayer.com' \
-H 'priority: u=1, i' \
-H 'referer: https://www.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' \
--data-raw '{"filters":{"term":{"sellerStatus":"Live","channelId":0,"language":["English"],"direct-seller":true,"directProduct":true,"listingType":"standard"},"range":{"quantity":{"gte":1},"direct-inventory":{"gte":1}},"exclude":{"channelExclusion":0,"listingType":"custom"}},"from":0,"size":1,"context":{"shippingCountry":"US","cart":{}},"sort":{"field":"price+shipping","order":"asc"}}'
AGGREGATION AND SOME SPECIFIC DATA IDK THIS MIGHT BE A GOOD ONE
curl 'https://mp-search-api.tcgplayer.com/v1/product/615745/listings?mpfev=3279' \
-H 'accept: application/json, text/plain, */*' \
-H 'accept-language: en-US,en;q=0.8' \
-H 'content-type: application/json' \
-b 'tcgpartner=PK=TRADECARDS&M=1; valid=set=true; product-display-settings=sort=price+shipping&size=10; OAuthLoginSessionId=63b1a89d-1ac2-43f7-8e79-55a9ca5e761d; __RequestVerificationToken_L2FkbWlu0=Lw1sfWh823UeJ7zRux0b1ZTI4Vg4i_dFt97a55aQpf-qBURVuwWDCJyuCxSwgLNLe9nPlfDSc1AMV5nyqhY4Q4jurxs1; spDisabledUIFeatures=orders; SellerProximity=ZipCode=&MaxSellerDistance=1000&IsActive=false; tcg-uuid=613192dc-ecf6-481a-bec1-afdee8686db7; LastSeller=e576ed4c; __RequestVerificationToken=VFv72VLK6McJVzthg8O-41p7BNkdoW2jQAlDAu-ylO39qfzCddRi2-7bWiH4qloc8Vo_ZftOAAa5OhXL3OByFHIdlwY1; TCGAuthTicket_Production=270B0566400C905C51DEAD644E3CDBD634ECBCCC796B1F77717F461EE104FCE101CFAD2A8458319330A0931018A99214D4EA5601E7551E25E2069ACA550BB71775C0A04F30724E2C4E262CB167EAC2C2EB05D15F9EA08363FC6455B94654F1F110CF079E24201C3B8CEF26762423D8CAA71DDF7B; ASP.NET_SessionId=5ycv15jf0mon3l5adodmkog5; StoreSaveForLater_PRODUCTION=SFLK=a167bf88521f4d0fbeb7497a7ed74629&Ignore=false; TCG_VisitorKey=81fe992f-9a12-4926-a417-7815c4f94edd; setting=CD=US&M=1; SearchSortSettings=M=1&ProductSortOption=MinPrice&ProductSortDesc=True&PriceSortOption=Shipping&ProductResultDisplay=grid; tcg_analytics_previousPageData=%7B%22title%22%3A%22Seller%20Feedback%22%2C%22href%22%3A%22https%3A%2F%2Fshop.tcgplayer.com%2Fsellerfeedback%2Fbe27fef9%22%7D; fileDownloadToken=1740499419709; StoreCart_PRODUCTION=CK=b4f8aff616974a12a6b2811129b81ee2&Ignore=false; 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}}; tcg-segment-session=1740595460137%257C1740595481430' \
-H 'origin: https://www.tcgplayer.com' \
-H 'priority: u=1, i' \
-H 'referer: https://www.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' \
--data-raw '{"filters":{"term":{"sellerStatus":"Live","channelId":0,"language":["English"]},"range":{"quantity":{"gte":1}},"exclude":{"channelExclusion":0}},"from":0,"size":10,"sort":{"field":"price+shipping","order":"asc"},"context":{"shippingCountry":"US","cart":{}},"aggregations":["listingType"]}'
AGGREGATION OF RANDOM SHIT IDK
curl 'https://mp-search-api.tcgplayer.com/v1/product/615745/listings?mpfev=3279' \
-H 'accept: application/json, text/plain, */*' \
-H 'accept-language: en-US,en;q=0.8' \
-H 'content-type: application/json' \
-b 'tcgpartner=PK=TRADECARDS&M=1; valid=set=true; product-display-settings=sort=price+shipping&size=10; OAuthLoginSessionId=63b1a89d-1ac2-43f7-8e79-55a9ca5e761d; __RequestVerificationToken_L2FkbWlu0=Lw1sfWh823UeJ7zRux0b1ZTI4Vg4i_dFt97a55aQpf-qBURVuwWDCJyuCxSwgLNLe9nPlfDSc1AMV5nyqhY4Q4jurxs1; spDisabledUIFeatures=orders; SellerProximity=ZipCode=&MaxSellerDistance=1000&IsActive=false; tcg-uuid=613192dc-ecf6-481a-bec1-afdee8686db7; LastSeller=e576ed4c; __RequestVerificationToken=VFv72VLK6McJVzthg8O-41p7BNkdoW2jQAlDAu-ylO39qfzCddRi2-7bWiH4qloc8Vo_ZftOAAa5OhXL3OByFHIdlwY1; TCGAuthTicket_Production=270B0566400C905C51DEAD644E3CDBD634ECBCCC796B1F77717F461EE104FCE101CFAD2A8458319330A0931018A99214D4EA5601E7551E25E2069ACA550BB71775C0A04F30724E2C4E262CB167EAC2C2EB05D15F9EA08363FC6455B94654F1F110CF079E24201C3B8CEF26762423D8CAA71DDF7B; ASP.NET_SessionId=5ycv15jf0mon3l5adodmkog5; StoreSaveForLater_PRODUCTION=SFLK=a167bf88521f4d0fbeb7497a7ed74629&Ignore=false; TCG_VisitorKey=81fe992f-9a12-4926-a417-7815c4f94edd; setting=CD=US&M=1; SearchSortSettings=M=1&ProductSortOption=MinPrice&ProductSortDesc=True&PriceSortOption=Shipping&ProductResultDisplay=grid; tcg_analytics_previousPageData=%7B%22title%22%3A%22Seller%20Feedback%22%2C%22href%22%3A%22https%3A%2F%2Fshop.tcgplayer.com%2Fsellerfeedback%2Fbe27fef9%22%7D; fileDownloadToken=1740499419709; StoreCart_PRODUCTION=CK=b4f8aff616974a12a6b2811129b81ee2&Ignore=false; 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}}; tcg-segment-session=1740595460137%257C1740595481430' \
-H 'origin: https://www.tcgplayer.com' \
-H 'priority: u=1, i' \
-H 'referer: https://www.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' \
--data-raw '{"filters":{"term":{"condition":["Near Mint","Lightly Played","Moderately Played","Heavily Played","Damaged"],"printing":["Foil"],"language":["English"],"sellerStatus":"Live"},"range":{"quantity":{"gte":1}},"exclude":{"channelExclusion":0}},"context":{"shippingCountry":"US","cart":{}},"aggregations":["seller-key"],"size":0}'
VOLATILITY
curl 'https://mpgateway.tcgplayer.com/v1/pricepoints/marketprice/skus/8547894/volatility?mpfev=3279' \
-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; OAuthLoginSessionId=63b1a89d-1ac2-43f7-8e79-55a9ca5e761d; __RequestVerificationToken_L2FkbWlu0=Lw1sfWh823UeJ7zRux0b1ZTI4Vg4i_dFt97a55aQpf-qBURVuwWDCJyuCxSwgLNLe9nPlfDSc1AMV5nyqhY4Q4jurxs1; spDisabledUIFeatures=orders; SellerProximity=ZipCode=&MaxSellerDistance=1000&IsActive=false; tcg-uuid=613192dc-ecf6-481a-bec1-afdee8686db7; LastSeller=e576ed4c; __RequestVerificationToken=VFv72VLK6McJVzthg8O-41p7BNkdoW2jQAlDAu-ylO39qfzCddRi2-7bWiH4qloc8Vo_ZftOAAa5OhXL3OByFHIdlwY1; TCGAuthTicket_Production=270B0566400C905C51DEAD644E3CDBD634ECBCCC796B1F77717F461EE104FCE101CFAD2A8458319330A0931018A99214D4EA5601E7551E25E2069ACA550BB71775C0A04F30724E2C4E262CB167EAC2C2EB05D15F9EA08363FC6455B94654F1F110CF079E24201C3B8CEF26762423D8CAA71DDF7B; ASP.NET_SessionId=5ycv15jf0mon3l5adodmkog5; StoreSaveForLater_PRODUCTION=SFLK=a167bf88521f4d0fbeb7497a7ed74629&Ignore=false; TCG_VisitorKey=81fe992f-9a12-4926-a417-7815c4f94edd; setting=CD=US&M=1; SearchSortSettings=M=1&ProductSortOption=MinPrice&ProductSortDesc=True&PriceSortOption=Shipping&ProductResultDisplay=grid; tcg_analytics_previousPageData=%7B%22title%22%3A%22Seller%20Feedback%22%2C%22href%22%3A%22https%3A%2F%2Fshop.tcgplayer.com%2Fsellerfeedback%2Fbe27fef9%22%7D; fileDownloadToken=1740499419709; StoreCart_PRODUCTION=CK=b4f8aff616974a12a6b2811129b81ee2&Ignore=false; 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}}; tcg-segment-session=1740595460137%257C1740595481430' \
-H 'origin: https://www.tcgplayer.com' \
-H 'priority: u=1, i' \
-H 'referer: https://www.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

@@ -0,0 +1,49 @@
import os
import logging
logger = logging.getLogger(__name__)
class DockerUtil:
def __init__(self):
pass
def is_in_docker(self) -> bool:
"""Check if we're running inside a Docker container using multiple methods"""
# Method 1: Check cgroup
try:
with open('/proc/1/cgroup', 'r') as f:
content = f.read().lower()
if any(container_id in content for container_id in ['docker', 'containerd', 'kubepods']):
logger.debug("Docker detected via cgroup")
return True
except Exception as e:
logger.debug(f"Could not read cgroup file: {e}")
# Method 2: Check /.dockerenv file
if os.path.exists('/.dockerenv'):
logger.debug("Docker detected via /.dockerenv file")
return True
# Method 3: Check environment variables
docker_env = any(os.environ.get(var, False) for var in [
'DOCKER_CONTAINER',
'IN_DOCKER',
'KUBERNETES_SERVICE_HOST', # For k8s
'DOCKER_HOST'
])
if docker_env:
logger.debug("Docker detected via environment variables")
return True
# Method 4: Check container runtime
try:
with open('/proc/self/mountinfo', 'r') as f:
content = f.read().lower()
if any(rt in content for rt in ['docker', 'containerd', 'kubernetes']):
logger.debug("Docker detected via mountinfo")
return True
except Exception as e:
logger.debug(f"Could not read mountinfo: {e}")
logger.debug("No Docker environment detected")
return False

View File

@@ -0,0 +1,149 @@
from typing import Dict, Optional
from app.services.util._docker import DockerUtil
from enum import Enum
import browser_cookie3
import os
import json
import requests
import time
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
class Browser(Enum):
"""Supported browser types for cookie extraction"""
BRAVE = "brave"
CHROME = "chrome"
FIREFOX = "firefox"
class Method(Enum):
"""Supported HTTP methods"""
GET = "GET"
POST = "POST"
class TCGPlayerEndpoints(Enum):
"""Supported TCGPlayer API endpoints"""
ORDERS = "https://order-management-api.tcgplayer.com/orders"
class Headers:
ACCEPT = 'application/json, text/plain, */*'
ACCEPT_ENCODING = 'gzip, deflate, br, zstd'
ACCEPT_LANGUAGE = 'en-US,en;q=0.8'
PRIORITY = 'u=1, i'
SECCHUA = '"Not(A:Brand";v="99", "Brave";v="133", "Chromium";v="133"'
SECCHUA_MOBILE = '?0'
SECCHUA_PLATFORM = '"macOS"'
SEC_FETCH_DEST = 'empty'
SEC_FETCH_MODE = 'cors'
SEC_FETCH_SITE = 'same-site'
SEC_GPC = '1'
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'
SELLER_ORIGIN = 'https://sellerportal.tcgplayer.com'
SELLER_REFERER = 'https://sellerportal.tcgplayer.com/'
class RequestHeaders:
BASE_HEADERS = {
'accept': Headers.ACCEPT,
'accept-encoding': Headers.ACCEPT_ENCODING,
'accept-language': Headers.ACCEPT_LANGUAGE,
'priority': Headers.PRIORITY,
'sec-ch-ua': Headers.SECCHUA,
'sec-ch-ua-mobile': Headers.SECCHUA_MOBILE,
'sec-ch-ua-platform': Headers.SECCHUA_PLATFORM,
'sec-fetch-dest': Headers.SEC_FETCH_DEST,
'sec-fetch-mode': Headers.SEC_FETCH_MODE,
'sec-fetch-site': Headers.SEC_FETCH_SITE,
'sec-gpc': Headers.SEC_GPC,
'user-agent': Headers.USER_AGENT
}
SELLER_HEADERS = {
'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):
self.browser_type = browser_type
self.docker_util = DockerUtil()
self.previous_request_time = datetime.now()
def get_session(self, cookies: Dict = None) -> requests.Session:
"""Create a session with the specified cookies"""
session = requests.Session()
if cookies:
session.cookies.update(cookies)
return session
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, files=files)
response.raise_for_status()
return response
except requests.RequestException as e:
logger.error(f"Request failed: {str(e)}")
return None
def get_tcgplayer_cookies_from_file(self) -> Dict:
# check if cookies file exists
if not os.path.exists('cookies/tcg_cookies.json'):
raise ValueError("Cookies file not found")
with open('cookies/tcg_cookies.json', 'r') as f:
logger.debug("Loading cookies from file")
cookies = json.load(f)
return cookies
def get_tcgplayer_browser_cookies(self) -> Optional[Dict]:
"""Retrieve cookies from the specified browser"""
try:
cookie_getter = getattr(browser_cookie3, self.browser_type.value, None)
if not cookie_getter:
raise ValueError(f"Unsupported browser type: {self.browser_type.value}")
return cookie_getter()
except Exception as e:
logger.error(f"Failed to get browser cookies: {str(e)}")
return None
def rate_limit(self, time_between_requests: int = 10):
"""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:
time.sleep(time_between_requests - time_diff)
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, method)
if not headers:
raise ValueError("Headers not set")
try:
self.rate_limit()
response = requests.request(method, url, headers=headers, cookies=cookies, data=data, json=json)
response.raise_for_status()
self.previous_request_time = datetime.now()
return response
except requests.RequestException as e:
logger.error(f"Request failed: {str(e)}")
return None
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 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}")

View File

@@ -2,16 +2,24 @@ curl -J http://192.168.1.41:8000/api/tcgplayer/inventory/update --remote-name
curl -J -X POST http://192.168.1.41:8000/api/tcgplayer/inventory/add \ curl -J -X POST http://192.168.1.41:8000/api/tcgplayer/inventory/add \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"open_box_ids": ["fb629d9d-13d2-405e-9a69-6c44294d55de"]}' \ -d '{"open_box_ids": ["f4a8e94c-5592-4b27-97b7-5cdb3eb45a71","81918f03-cbd2-4129-9f5c-eada6e1a811f","c2083c29-6fac-4621-b4b6-30c2dac75fab", "4ac89a6b-b8b7-44cf-8a4e-4a95c8e9006d", "0e809522-cef6-4c3c-b8a3-742c2e3c83fd","9e68466f-5abb-4725-9da8-91e5aaa4e805"]}' \
--remote-name --remote-name
curl -X POST http://192.168.1.41:8000/api/boxes \ curl -X POST http://192.168.1.41:8000/api/boxes \
-F "type=draft" \ -F "type=play" \
-F "set_code=CLB" \ -F "set_code=TDM" \
-F "sku=195166181127" \ -F "sku=1234" \
-F "num_cards_expected=480" -F "num_cards_expected=420"
curl -X POST "http://192.168.1.41:8000/api/boxes/0d31b9c3-3093-438a-9e8c-b6b70a2d437e/open" \ curl -X POST "http://192.168.1.41:8000/api/boxes/a77194be-8bd6-41cc-89a0-820e92ef9c04/open" \
-F "product_id=0d31b9c3-3093-438a-9e8c-b6b70a2d437e" \ -F "product_id=a77194be-8bd6-41cc-89a0-820e92ef9c04" \
-F "file_ids=bb4a022c-5427-49b5-b57c-0147b5e9c4a9" \ -F "file_ids=b11a0292-bfdc-43de-90a8-6eb383332201" \
-F "date_opened=2025-02-15" -F "date_opened=2025-04-14"
curl -X POST "http://192.168.1.41:8000/api/processOrders" \
-H "Content-Type: application/json" \
-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 annotated-types==0.7.0
anyio==4.8.0 anyio==4.8.0
APScheduler==3.11.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 browser-cookie3==0.20.1
certifi==2025.1.31 certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1 charset-normalizer==3.4.1
click==8.1.8 click==8.1.8
coverage==7.6.10 coverage==7.6.10
cssselect2==0.8.0
fastapi==0.115.8 fastapi==0.115.8
fonttools==4.57.0
future==1.0.0
greenlet==3.1.1
h11==0.14.0 h11==0.14.0
httpcore==1.0.7 httpcore==1.0.7
httpx==0.28.1 httpx==0.28.1
idna==3.10 idna==3.10
iniconfig==2.0.0 iniconfig==2.0.0
jeepney==0.9.0
Jinja2==3.1.6
jsons==1.6.3
lz4==4.4.3 lz4==4.4.3
Mako==1.3.9 Mako==1.3.9
MarkupSafe==3.0.2 MarkupSafe==3.0.2
numpy==2.2.2 numpy==2.2.2
packaging==24.2 packaging==24.2
packbits==0.6
pandas==2.2.3 pandas==2.2.3
pdf2image==1.17.0
pillow==11.1.0
pluggy==1.5.0 pluggy==1.5.0
psycopg2-binary==2.9.10 psycopg2-binary==2.9.10
pycparser==2.22
pycryptodomex==3.21.0 pycryptodomex==3.21.0
pydantic==2.10.6 pydantic==2.10.6
pydantic_core==2.27.2 pydantic_core==2.27.2
pydyf==0.11.0
pyphen==0.17.2
pyStrich==0.9
pytest==8.3.4 pytest==8.3.4
pytest-asyncio==0.25.3 pytest-asyncio==0.25.3
pytest-cov==6.0.0 pytest-cov==6.0.0
python-barcode==0.15.1
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
python-multipart==0.0.20 python-multipart==0.0.20
pytz==2025.1 pytz==2025.1
pyusb==1.3.1
qrcode==8.1
requests==2.32.3 requests==2.32.3
six==1.17.0 six==1.17.0
sniffio==1.3.1 sniffio==1.3.1
SQLAlchemy==2.0.37 SQLAlchemy==2.0.37
starlette==0.45.3 starlette==0.45.3
tinycss2==1.4.0
tinyhtml5==2.0.0
typing_extensions==4.12.2 typing_extensions==4.12.2
typish==1.9.3
tzdata==2025.1 tzdata==2025.1
tzlocal==5.2 tzlocal==5.2
urllib3==2.3.0 urllib3==2.3.0
uvicorn==0.34.0 uvicorn==0.34.0
weasyprint==65.0
webencodings==0.5.1
zopfli==0.2.3.post1