addresses and pricing
All checks were successful
Deploy App to Docker / deploy (push) Successful in 1m30s
All checks were successful
Deploy App to Docker / deploy (push) Successful in 1m30s
This commit is contained in:
parent
f59f0f350c
commit
9021151b74
@ -4,6 +4,11 @@ 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 fonts-liberation && \
|
||||||
|
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
|
||||||
|
BIN
app/assets/images/ccrcardsaddress.png
Normal file
BIN
app/assets/images/ccrcardsaddress.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
87
app/assets/templates/address_label.html
Normal file
87
app/assets/templates/address_label.html
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
/* Setting up the page size - 4x6 inches */
|
||||||
|
@page {
|
||||||
|
size: 4in 6in;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force page breaks after each label */
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 4in;
|
||||||
|
height: 6in;
|
||||||
|
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: 1.5in;
|
||||||
|
height: 0.46in;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stamp area in top right */
|
||||||
|
.stamp-area {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.25in;
|
||||||
|
right: 0.25in;
|
||||||
|
width: 1in;
|
||||||
|
height: 1in;
|
||||||
|
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>
|
@ -317,18 +317,16 @@ async def update_cookies(
|
|||||||
detail=f"Failed to update cookies: {str(e)}"
|
detail=f"Failed to update cookies: {str(e)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
### DEPRECATED ###
|
|
||||||
class TCGPlayerOrderRequest(BaseModel):
|
class TCGPlayerOrderRequest(BaseModel):
|
||||||
order_ids: List[str]
|
order_ids: List[str]
|
||||||
|
|
||||||
@router.post("/processOrders", response_model=ProcessOrdersResponse)
|
@router.get("/processOrders", response_model=dict)
|
||||||
async def process_orders(
|
async def process_orders(
|
||||||
body: TCGPlayerOrderRequest,
|
|
||||||
tcgplayer_api_service: TCGPlayerAPIService = Depends(get_tcgplayer_api_service),
|
tcgplayer_api_service: TCGPlayerAPIService = Depends(get_tcgplayer_api_service),
|
||||||
) -> ProcessOrdersResponse:
|
) -> ProcessOrdersResponse:
|
||||||
"""Process TCGPlayer orders."""
|
"""Process TCGPlayer orders."""
|
||||||
try:
|
try:
|
||||||
orders = tcgplayer_api_service.process_orders(body.order_ids)
|
orders = tcgplayer_api_service.process_open_orders()
|
||||||
return ProcessOrdersResponse(
|
return ProcessOrdersResponse(
|
||||||
status_code=200,
|
status_code=200,
|
||||||
success=True,
|
success=True,
|
||||||
@ -337,5 +335,3 @@ async def process_orders(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Process orders failed: {str(e)}")
|
logger.error(f"Process orders failed: {str(e)}")
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
### END DEPRECATED ###
|
|
@ -8,10 +8,14 @@ from app.db.utils import db_transaction
|
|||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
from decimal import Decimal, ROUND_HALF_UP
|
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):
|
||||||
@ -115,6 +119,98 @@ class PricingService:
|
|||||||
row[price_type] = price
|
row[price_type] = price
|
||||||
return row
|
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:
|
def default_pricing_algo(self, row: pd.Series) -> pd.Series:
|
||||||
"""Default pricing algorithm with complex pricing rules"""
|
"""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:
|
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 is None or ACTIVE_PRICING_ALGORITHIM == 'default_pricing_algo':
|
||||||
pricing_algo = self.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)
|
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:
|
||||||
|
@ -7,6 +7,8 @@ from app.db.utils import db_transaction
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from uuid import uuid4 as uuid
|
from uuid import uuid4 as uuid
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
from weasyprint import HTML
|
||||||
import json
|
import json
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -27,6 +29,10 @@ class TCGPlayerAPIService:
|
|||||||
self.config = TCGPlayerAPIConfig()
|
self.config = TCGPlayerAPIConfig()
|
||||||
self.cookies = self.get_cookies()
|
self.cookies = self.get_cookies()
|
||||||
self.session = None
|
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 = "/app/app/assets/images/ccrcardsaddress.png"
|
||||||
|
|
||||||
def get_cookies(self) -> dict:
|
def get_cookies(self) -> dict:
|
||||||
if self.is_in_docker:
|
if self.is_in_docker:
|
||||||
@ -228,15 +234,120 @@ class TCGPlayerAPIService:
|
|||||||
"sortingType": "byRelease",
|
"sortingType": "byRelease",
|
||||||
"format": "default",
|
"format": "default",
|
||||||
"timezoneOffset": -4,
|
"timezoneOffset": -4,
|
||||||
"orderNumbers": [order_ids]
|
"orderNumbers": order_ids
|
||||||
}
|
}
|
||||||
response = self.requests_util.send_request(url, method='POST', cookies=self.cookies, json=payload)
|
response = self.requests_util.send_request(url, method='POST', cookies=self.cookies, json=payload)
|
||||||
if response:
|
if response:
|
||||||
# get filename from response headers
|
# get filename from response headers
|
||||||
filename = response.headers.get('Content-Disposition').split('filename=')[1].strip('"')
|
filename = response.headers.get('Content-Disposition').split('filename=')[1].strip('"')
|
||||||
# save file to disk
|
# save file to disk
|
||||||
with open(filename, 'wb') as f:
|
with open('/app/tmp' + filename, 'wb') as f:
|
||||||
f.write(response.content)
|
f.write(response.content)
|
||||||
|
|
||||||
|
return 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
|
||||||
|
filename = response.headers.get('Content-Disposition').split('filename=')[1].strip('"')
|
||||||
|
# save file to disk
|
||||||
|
with open('/app/tmp' + filename, 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
|
||||||
|
return filename
|
||||||
|
|
||||||
|
def get_address_labels_pdf(self, order_ids: list[str]):
|
||||||
|
labels_html = []
|
||||||
|
|
||||||
|
for order_id in order_ids:
|
||||||
|
order = self.get_order(order_id)
|
||||||
|
|
||||||
|
if order:
|
||||||
|
try:
|
||||||
|
# Extract relevant information from the order
|
||||||
|
order_info = {
|
||||||
|
"recipient_name": order['shippingAddress']['recipientName'],
|
||||||
|
"address_line1": order['shippingAddress']['addressOne'],
|
||||||
|
"address_line2": order['shippingAddress'].get('addressTwo', ''),
|
||||||
|
"city": order['shippingAddress']['city'],
|
||||||
|
"state": order['shippingAddress']['territory'],
|
||||||
|
"zip_code": order['shippingAddress']['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))
|
||||||
|
except KeyError as e:
|
||||||
|
print(f"Missing field in order {order_id}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
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):
|
||||||
|
# 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:
|
||||||
|
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="tcgportal.local:8000/upload",
|
||||||
|
method='POST',
|
||||||
|
files=file
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,23 +2,32 @@ 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
|
||||||
|
brother_ql_next==0.11.3
|
||||||
browser-cookie3==0.20.1
|
browser-cookie3==0.20.1
|
||||||
certifi==2025.1.31
|
certifi==2025.1.31
|
||||||
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
|
||||||
fastapi==0.115.8
|
fastapi==0.115.8
|
||||||
|
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
|
||||||
|
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
|
||||||
pycryptodomex==3.21.0
|
pycryptodomex==3.21.0
|
||||||
@ -30,12 +39,14 @@ pytest-cov==6.0.0
|
|||||||
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
|
||||||
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
|
||||||
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user