labels and stuff

This commit is contained in:
zman 2025-04-13 21:11:55 -04:00
parent 56c2d1de47
commit 18b32c8514
26 changed files with 1627 additions and 25 deletions

3
.gitignore vendored
View File

@ -13,4 +13,5 @@ app/data/cache
*.log
.env
.env
alembic.ini

View File

@ -58,7 +58,7 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = sqlite:///database.db
sqlalchemy.url = postgresql://poggers:giga!@192.168.1.41:5432/ai_giga_tcg
[post_write_hooks]

View File

@ -73,7 +73,6 @@ def upgrade() -> None:
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['card_id'], ['mtgjson_cards.card_id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_mtgjson_skus_card_id'), 'mtgjson_skus', ['card_id'], unique=False)

View File

@ -0,0 +1,29 @@
"""fix alembic version table
Revision ID: 2025_04_14_fix_alembic_version
Revises: 4dbeb89dd33a
Create Date: 2025-04-14 00:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2025_04_14_fix_alembic_version'
down_revision = '4dbeb89dd33a'
branch_labels = None
depends_on = None
def upgrade() -> None:
# Increase the size of version_num column in alembic_version table
op.alter_column('alembic_version', 'version_num',
existing_type=sa.String(32),
type_=sa.String(255))
def downgrade() -> None:
# Revert the column size back to 32
op.alter_column('alembic_version', 'version_num',
existing_type=sa.String(255),
type_=sa.String(32))

View File

@ -0,0 +1,32 @@
"""fix foreign key issue
Revision ID: fix_foreign_key_issue
Revises: 5bf5f87793d7
Create Date: 2025-04-14 04:15:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'fix_foreign_key_issue'
down_revision: Union[str, None] = '5bf5f87793d7'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Drop the foreign key constraint if it exists
op.execute('ALTER TABLE mtgjson_skus DROP CONSTRAINT IF EXISTS mtgjson_skus_card_id_fkey')
# Make the column nullable
op.alter_column('mtgjson_skus', 'card_id',
existing_type=sa.String(),
nullable=True)
def downgrade() -> None:
# No downgrade - we don't want to recreate the constraint
pass

View File

@ -0,0 +1,33 @@
"""fix mtgjson final
Revision ID: 2025_04_14_fix_mtgjson_final
Revises: d1628d8feb57
Create Date: 2025-04-14 00:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2025_04_14_fix_mtgjson_final'
down_revision = 'd1628d8feb57'
branch_labels = None
depends_on = None
def upgrade() -> None:
# Drop the foreign key constraint and make card_id nullable
op.drop_constraint('mtgjson_skus_card_id_fkey', 'mtgjson_skus', type_='foreignkey')
op.alter_column('mtgjson_skus', 'card_id',
existing_type=sa.String(),
nullable=True)
def downgrade() -> None:
# Make card_id not nullable and recreate foreign key
op.alter_column('mtgjson_skus', 'card_id',
existing_type=sa.String(),
nullable=False)
op.create_foreign_key('mtgjson_skus_card_id_fkey',
'mtgjson_skus', 'mtgjson_cards',
['card_id'], ['card_id'])

View File

@ -0,0 +1,33 @@
"""fix mtgjson foreign key
Revision ID: 2025_04_14_fix_mtgjson_foreign_key
Revises: 4ad81b486caf
Create Date: 2025-04-14 00:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2025_04_14_fix_mtgjson_foreign_key'
down_revision = '4ad81b486caf'
branch_labels = None
depends_on = None
def upgrade() -> None:
# Drop the foreign key constraint and make card_id nullable
op.execute('ALTER TABLE mtgjson_skus DROP CONSTRAINT IF EXISTS mtgjson_skus_card_id_fkey')
op.alter_column('mtgjson_skus', 'card_id',
existing_type=sa.String(),
nullable=True)
def downgrade() -> None:
# Make card_id not nullable and recreate foreign key
op.alter_column('mtgjson_skus', 'card_id',
existing_type=sa.String(),
nullable=False)
op.create_foreign_key('mtgjson_skus_card_id_fkey',
'mtgjson_skus', 'mtgjson_cards',
['card_id'], ['card_id'])

View File

@ -0,0 +1,31 @@
"""remove mtgjson foreign key constraint
Revision ID: 2025_04_14_remove_mtgjson_foreign_key
Revises: 2025_04_14_remove_mtgjson_data_columns
Create Date: 2025-04-14 00:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2025_04_14_remove_mtgjson_foreign_key'
down_revision = '2025_04_14_remove_mtgjson_data_columns'
branch_labels = None
depends_on = None
def upgrade() -> None:
# Drop the foreign key constraint from mtgjson_skus table
op.drop_constraint('mtgjson_skus_card_id_fkey', 'mtgjson_skus', type_='foreignkey')
def downgrade() -> None:
# Recreate the foreign key constraint
op.create_foreign_key(
'mtgjson_skus_card_id_fkey',
'mtgjson_skus',
'mtgjson_cards',
['card_id'],
['card_id']
)

View File

@ -0,0 +1,26 @@
"""merge all heads
Revision ID: 5bf5f87793d7
Revises: 2025_04_14_fix_alembic_version, 2025_04_14_fix_mtgjson_final
Create Date: 2025-04-13 00:12:47.613416
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '5bf5f87793d7'
down_revision: Union[str, None] = ('2025_04_14_fix_alembic_version', '2025_04_14_fix_mtgjson_final')
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
pass
def downgrade() -> None:
pass

View File

@ -0,0 +1,26 @@
"""merge heads
Revision ID: d1628d8feb57
Revises: 2025_04_14_fix_mtgjson_foreign_key, 2025_04_14_remove_mtgjson_foreign_key
Create Date: 2025-04-13 00:11:03.312552
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'd1628d8feb57'
down_revision: Union[str, None] = ('2025_04_14_fix_mtgjson_foreign_key', '2025_04_14_remove_mtgjson_foreign_key')
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
pass
def downgrade() -> None:
pass

20
app.log
View File

@ -1,2 +1,18 @@
2025-04-12 23:48:13,221 - INFO - app.main - Application starting up...
2025-04-12 23:48:16,568 - INFO - app.main - Database initialized successfully
2025-04-13 17:07:45,221 - INFO - app.main - Application starting up...
2025-04-13 17:07:45,277 - INFO - app.main - Database initialized successfully
2025-04-13 17:07:51,378 - INFO - app.services.label_printer_service - Converting PDF app/data/cache/tcgplayer/orders/packing_slip_2025-04-13.pdf to images
2025-04-13 17:07:51,467 - INFO - app.services.label_printer_service - Successfully converted PDF to 3 images
2025-04-13 17:07:51,467 - INFO - app.services.label_printer_service - Processing page 1 with dimensions (1700, 2200)
2025-04-13 17:08:10,149 - INFO - app.services.label_printer_service - Processing page 2 with dimensions (1700, 2200)
2025-04-13 17:08:14,940 - INFO - app.services.label_printer_service - Processing page 3 with dimensions (1700, 2200)
2025-04-13 17:08:19,947 - INFO - app.services.label_printer_service - Converting PDF app/data/cache/tcgplayer/address_labels/E576ED4C-491FAE-B9A96_dk1241.pdf to images
2025-04-13 17:08:19,992 - INFO - app.services.label_printer_service - Successfully converted PDF to 1 images
2025-04-13 17:08:19,992 - INFO - app.services.label_printer_service - Processing page 1 with dimensions (1200, 800)
2025-04-13 17:08:23,373 - INFO - app.services.label_printer_service - Converting PDF app/data/cache/tcgplayer/address_labels/E576ED4C-F69424-87265_dk1241.pdf to images
2025-04-13 17:08:23,415 - INFO - app.services.label_printer_service - Successfully converted PDF to 1 images
2025-04-13 17:08:23,415 - INFO - app.services.label_printer_service - Processing page 1 with dimensions (1200, 800)
2025-04-13 17:08:26,780 - INFO - app.services.label_printer_service - Converting PDF app/data/cache/tcgplayer/address_labels/E576ED4C-CD078B-CE881_dk1241.pdf to images
2025-04-13 17:08:26,822 - INFO - app.services.label_printer_service - Successfully converted PDF to 1 images
2025-04-13 17:08:26,823 - INFO - app.services.label_printer_service - Processing page 1 with dimensions (1200, 800)
2025-04-13 17:08:30,128 - INFO - app.main - TCGPlayer data initialized successfully
2025-04-13 17:08:30,128 - INFO - app.main - Scheduler started successfully

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
/* Setting up the page size for DK1201 label - 90x29mm */
@page {
size: 90mm 29mm;
margin: 0;
}
/* Force page breaks after each label */
body {
margin: 0;
padding: 0;
width: 90mm;
height: 29mm;
position: relative;
font-family: Arial, sans-serif;
}
.label-container {
width: 100%;
height: 100%;
position: relative;
box-sizing: border-box;
padding: 2mm;
page-break-after: always;
}
/* Main address centered */
.address {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 85mm;
text-align: center;
}
.recipient-name {
font-size: 10pt;
font-weight: bold;
margin-bottom: 1mm;
}
.address-line {
font-size: 9pt;
line-height: 1.2;
margin-bottom: 0.5mm;
}
.city-line {
font-size: 9pt;
font-weight: bold;
text-transform: uppercase;
}
</style>
</head>
<body>
<div class="label-container">
<div class="address">
<div class="recipient-name">{{ recipient_name }}</div>
<div class="address-line">{{ address_line1 }}</div>
{% if address_line2 %}<div class="address-line">{{ address_line2 }}</div>{% endif %}
<div class="city-line">{{ city }}, {{ state }} {{ zip_code }}</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<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;
object-fit: contain;
}
/* 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: 3.5in;
text-align: center;
}
.recipient-name {
font-size: 14pt;
font-weight: bold;
margin-bottom: 0.1in;
}
.address-line {
font-size: 12pt;
line-height: 1.3;
margin-bottom: 0.05in;
}
.city-line {
font-size: 12pt;
font-weight: bold;
text-transform: uppercase;
}
</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">
<div class="recipient-name">{{ recipient_name }}</div>
<div class="address-line">{{ address_line1 }}</div>
{% if address_line2 %}<div class="address-line">{{ address_line2 }}</div>{% endif %}
<div class="city-line">{{ city }}, {{ state }} {{ zip_code }}</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,136 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
@page {
size: letter;
margin: 0.5cm;
}
body {
font-family: Arial, sans-serif;
line-height: 1.2;
font-size: 9pt;
}
.header {
text-align: center;
margin-bottom: 10px;
}
.header h1 {
margin-bottom: 2px;
font-size: 12pt;
}
.header p {
color: #666;
margin: 0;
font-size: 8pt;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
border: 2px solid #000;
}
th {
background-color: #f5f5f5;
padding: 4px;
text-align: left;
border: 1px solid #000;
font-size: 8pt;
font-weight: bold;
}
td {
padding: 3px;
border: 1px solid #000;
font-size: 9pt;
}
tr:hover {
background-color: #f9f9f9;
}
.foil {
font-weight: bold;
background-color: #e0e0e0;
}
.multiple {
font-style: italic;
}
.foil.multiple {
font-weight: bold;
font-style: italic;
background-color: #e0e0e0;
}
.quantity {
text-align: center;
width: 30px;
}
.set {
width: 150px;
}
.condition {
width: 80px;
}
.rarity {
width: 30px;
text-align: center;
}
.product-name {
width: 200px;
}
/* Add alternating row colors */
tbody tr:nth-child(even) {
background-color: #f0f0f0;
}
/* Ensure hover effect works with alternating colors */
tbody tr:hover {
background-color: #e0e0e0;
}
</style>
</head>
<body>
<div class="header">
<h1>Pull Sheet</h1>
<p>Generated on {{ generation_date }}</p>
</div>
<table>
<thead>
<tr>
<th class="product-name">Product Name</th>
<th class="condition">Condition</th>
<th class="quantity">Qty</th>
<th class="set">Set</th>
<th class="rarity">Rarity</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr class="{{ 'foil' if 'Foil' in item.condition else '' }} {{ 'multiple' if item.quantity|int > 1 else '' }}">
<td class="product-name">{{ item.product_name }}</td>
<td class="condition">{{ item.condition }}</td>
<td class="quantity">{{ item.quantity }}</td>
<td class="set">{{ item.set }}</td>
<td class="rarity">{{ item.rarity }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
</html>

View File

@ -9,6 +9,12 @@ from app.db.database import init_db, SessionLocal
from app.services.scheduler.scheduler_service import SchedulerService
from app.services.data_initialization import DataInitializationService
from datetime import datetime
from app.services.external_api.tcgplayer.order_management_service import OrderManagementService
from app.services.address_label_service import AddressLabelService
from app.services.pull_sheet_service import PullSheetService
from app.services.print_service import PrintService
from app.services.label_printer_service import LabelPrinterService
from app.services.regular_printer_service import RegularPrinterService
# Configure logging
log_file = "app.log"
if os.path.exists(log_file):
@ -37,7 +43,12 @@ logger.info("Application starting up...")
# Initialize scheduler service
scheduler_service = SchedulerService()
data_init_service = DataInitializationService()
order_management_service = OrderManagementService()
address_label_service = AddressLabelService()
pull_sheet_service = PullSheetService()
#print_service = PrintService(printer_name="MFCL2750DW-3", printer_api_url="http://192.168.1.110:8000/print")
label_printer_service = LabelPrinterService(printer_api_url="http://192.168.1.110:8000")
regular_printer_service = RegularPrinterService(printer_name="MFCL2750DW-3")
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
@ -47,16 +58,52 @@ async def lifespan(app: FastAPI):
# Initialize TCGPlayer data
db = SessionLocal()
try:
await data_init_service.initialize_data(db, game_ids=[1, 3], init_archived_prices=False, archived_prices_start_date="2025-04-01", archived_prices_end_date=datetime.now().strftime("%Y-%m-%d"), init_categories=False, init_groups=False, init_products=False) # 1 = Magic, 3 = Pokemon
#await data_init_service.initialize_data(db, game_ids=[1, 3], init_archived_prices=False, archived_prices_start_date="2025-01-01", archived_prices_end_date=datetime.now().strftime("%Y-%m-%d"), init_categories=True, init_groups=True, init_products=True) # 1 = Magic, 3 = Pokemon
orders = await order_management_service.get_orders()
ready_orders = [order for order in orders if order.get("orderStatus") == "Ready to Ship"]
#logger.info(ready_orders)
order_ids = [order.get("orderNumber") for order in ready_orders]
# get only the first 3 order ids
order_ids = order_ids[:3]
#logger.info(order_ids)
packing_slip = await order_management_service.get_packing_slip(order_ids)
packing_slip_file = await order_management_service.save_file(packing_slip, f"packing_slip_{datetime.now().strftime('%Y-%m-%d')}.pdf")
await label_printer_service.print_file(packing_slip_file, label_size="dk1241", label_type="packing_slip")
#pull_sheet = await order_management_service.get_pull_sheet(order_ids)
#pull_sheet_file = await order_management_service.save_file(pull_sheet, f"pull_sheet_{datetime.now().strftime('%Y-%m-%d')}.csv")
#await regular_printer_service.print_file(pull_sheet_file)
shipping_csv = await order_management_service.get_shipping_csv(order_ids)
shipping_csv_file = await order_management_service.save_file(shipping_csv, f"shipping_csv_{datetime.now().strftime('%Y-%m-%d')}.csv")
# Wait for the file to be saved before generating labels
if not shipping_csv_file:
logger.error("Failed to save shipping CSV file")
return
shipping_labels_dk1241 = address_label_service.generate_labels_from_csv(shipping_csv_file, label_type="dk1241")
if not shipping_labels_dk1241:
logger.error("Failed to generate shipping labels")
return
for label in shipping_labels_dk1241:
if not label:
logger.error("Empty label path in shipping labels list")
continue
await label_printer_service.print_file(label, label_size="dk1241", label_type="address_label")
logger.info("TCGPlayer data initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize TCGPlayer data: {str(e)}")
finally:
db.close()
# Start the scheduler
await scheduler_service.start_scheduled_tasks()
await scheduler_service.process_tcgplayer_export(export_type="live", use_cache=True)
#await scheduler_service.start_scheduled_tasks()
#await scheduler_service.process_tcgplayer_export(export_type="live", use_cache=True)
logger.info("Scheduler started successfully")
yield

View File

@ -1,6 +1,5 @@
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from app.db.database import Base
class MTGJSONCard(Base):
@ -41,7 +40,4 @@ class MTGJSONCard(Base):
tnt_id = Column(String, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.current_timestamp())
updated_at = Column(DateTime(timezone=True), onupdate=func.current_timestamp())
# Relationships
skus = relationship("MTGJSONSKU", back_populates="card")
updated_at = Column(DateTime(timezone=True), onupdate=func.current_timestamp())

View File

@ -1,6 +1,5 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from app.db.database import Base
class MTGJSONSKU(Base):
@ -13,9 +12,6 @@ class MTGJSONSKU(Base):
finish = Column(String)
language = Column(String)
printing = Column(String)
card_id = Column(String, ForeignKey("mtgjson_cards.card_id"))
card_id = Column(String, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.current_timestamp())
updated_at = Column(DateTime(timezone=True), onupdate=func.current_timestamp())
# Relationships
card = relationship("MTGJSONCard", back_populates="skus")

View File

@ -0,0 +1,77 @@
from typing import List, Dict, Optional, Literal
import csv
import os
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
from weasyprint import HTML
class AddressLabelService:
def __init__(self):
self.template_dir = Path("app/data/assets/templates")
self.env = Environment(loader=FileSystemLoader(str(self.template_dir)))
self.templates = {
"dk1241": self.env.get_template("address_label_dk1241.html"),
"dk1201": self.env.get_template("address_label_dk1201.html")
}
self.return_address_path = "file://" + os.path.abspath("app/data/assets/images/ccrcardsaddress.png")
self.output_dir = "app/data/cache/tcgplayer/address_labels/"
os.makedirs(self.output_dir, exist_ok=True)
def generate_labels_from_csv(self, csv_path: str, label_type: Literal["dk1201", "dk1241"]) -> List[str]:
"""Generate address labels from a CSV file and save them as PDFs.
Args:
csv_path: Path to the CSV file containing address data
label_type: Type of label to generate ("6x4" or "dk1201")
Returns:
List of paths to generated PDF files
"""
generated_files = []
with open(csv_path, 'r') as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
# Generate label for each row
pdf_path = self._generate_single_label(row, label_type)
if pdf_path:
generated_files.append(str(pdf_path))
return generated_files
def _generate_single_label(self, row: Dict[str, str], label_type: Literal["dk1201", "dk1241"]) -> Optional[str]:
"""Generate a single address label PDF.
Args:
row: Dictionary containing address data
label_type: Type of label to generate ("6x4" or "dk1201")
Returns:
Path to the generated PDF file or None if generation failed
"""
try:
# Prepare template data
template_data = {
"recipient_name": f"{row['FirstName']} {row['LastName']}",
"address_line1": row['Address1'],
"address_line2": row['Address2'],
"city": row['City'],
"state": row['State'],
"zip_code": row['PostalCode']
}
# Add return address path only for 6x4 labels
if label_type == "dk1241":
template_data["return_address_path"] = self.return_address_path
# Render HTML
html_content = self.templates[label_type].render(**template_data)
# Generate PDF
pdf_path = self.output_dir + f"{row['Order #']}_{label_type}.pdf"
HTML(string=html_content).write_pdf(str(pdf_path))
return pdf_path
except Exception as e:
print(f"Error generating label for order {row.get('Order #', 'unknown')}: {str(e)}")
return None

View File

@ -23,13 +23,19 @@ class BaseTCGPlayerService(BaseExternalService):
self.credentials = TCGPlayerCredentials()
def _get_headers(self, method: str) -> Dict[str, str]:
"""Get headers based on the HTTP method"""
def _get_headers(self, method: str, content_type: str = 'application/x-www-form-urlencoded') -> Dict[str, str]:
"""Get headers based on the HTTP method and content type
Args:
method: HTTP method (GET, POST, etc.)
content_type: Content type for the request. Defaults to 'application/x-www-form-urlencoded'
"""
base_headers = {
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'accept-language': 'en-US,en;q=0.8',
'priority': 'u=0, i',
'referer': 'https://store.tcgplayer.com/admin/pricing',
'referer': 'https://sellerportal.tcgplayer.com/',
'origin': 'https://sellerportal.tcgplayer.com',
'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Brave";v="132"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
@ -45,8 +51,7 @@ class BaseTCGPlayerService(BaseExternalService):
if method == 'POST':
post_headers = {
'cache-control': 'max-age=0',
'content-type': 'application/x-www-form-urlencoded',
'origin': 'https://store.tcgplayer.com'
'content-type': content_type
}
base_headers.update(post_headers)

View File

@ -0,0 +1,81 @@
from typing import Any, Dict, Optional, Union
import logging
from app.services.external_api.tcgplayer.base_tcgplayer_service import BaseTCGPlayerService
import os
logger = logging.getLogger(__name__)
class OrderManagementService(BaseTCGPlayerService):
ORDER_MANAGEMENT_BASE_URL = "https://order-management-api.tcgplayer.com/orders"
def __init__(self):
super().__init__()
self.base_url = self.ORDER_MANAGEMENT_BASE_URL
self.API_VERSION: str = "?api-version=2.0"
self.SELLER_KEY: str = "e576ed4c"
self.order_search_endpoint = f"/search{self.API_VERSION}"
self.packing_slip_endpoint = f"/packing-slips/export{self.API_VERSION}"
self.pull_sheet_endpoint = f"/pull-sheets/export{self.API_VERSION}"
self.shipping_endpoint = f"/shipping/export{self.API_VERSION}"
async def get_orders(self):
search_from = 0
orders = []
while True:
payload = {
"searchRange": "LastThreeMonths",
"filters": {
"sellerKey": self.SELLER_KEY
},
"sortBy": [
{"sortingType": "orderStatus", "direction": "ascending"},
{"sortingType": "orderDate", "direction": "descending"}
],
"from": search_from,
"size": 25
}
response = await self._make_request("POST", self.order_search_endpoint, data=payload, headers=self._get_headers("POST", "application/json"), auth_required=True)
if len(response.get("orders")) == 0:
break
search_from += 25
orders.extend(response.get("orders"))
return orders
async def get_order(self, order_id: str):
response = await self._make_request("GET", f"{self.ORDER_MANAGEMENT_BASE_URL}/{order_id}{self.API_VERSION}")
return response
async def get_packing_slip(self, order_ids: list[str]):
payload = {
"sortingType": "byRelease",
"format": "default",
"timezoneOffset": -4,
"orderNumbers": order_ids
}
response = await self._make_request("POST", self.packing_slip_endpoint, data=payload, headers=self._get_headers("POST", "application/json"), auth_required=True, download_file=True)
return response
async def get_pull_sheet(self, order_ids: list[str]):
payload = {
"orderNumbers": order_ids,
"timezoneOffset": -4
}
response = await self._make_request("POST", self.pull_sheet_endpoint, data=payload, headers=self._get_headers("POST", "application/json"), auth_required=True, download_file=True)
return response
async def get_shipping_csv(self, order_ids: list[str]):
payload = {
"orderNumbers": order_ids,
"timezoneOffset": -4
}
response = await self._make_request("POST", self.shipping_endpoint, data=payload, headers=self._get_headers("POST", "application/json"), auth_required=True, download_file=True)
return response
async def save_file(self, file_data: bytes, file_name: str) -> str:
if not os.path.exists("app/data/cache/tcgplayer/orders"):
os.makedirs("app/data/cache/tcgplayer/orders")
file_path = f"app/data/cache/tcgplayer/orders/{file_name}"
with open(file_path, "wb") as f:
f.write(file_data)
return file_path

View File

@ -0,0 +1,231 @@
from typing import Optional, Union, Literal
import aiohttp
from pathlib import Path
from pdf2image import convert_from_path
from brother_ql.conversion import convert
from brother_ql.raster import BrotherQLRaster
import logging
import asyncio
import time
from PIL import Image
from contextlib import asynccontextmanager
logger = logging.getLogger(__name__)
class LabelPrinterService:
def __init__(self, printer_api_url: str = "http://localhost:8000"):
"""Initialize the label printer service.
Args:
printer_api_url: Base URL of the printer API endpoint
"""
self.printer_api_url = printer_api_url.rstrip('/')
self.status_url = f"{self.printer_api_url}/status"
self.print_url = f"{self.printer_api_url}/print"
self.cache_dir = Path("app/data/cache/prints")
self.cache_dir.mkdir(parents=True, exist_ok=True)
self._session = None
self._lock = asyncio.Lock()
@asynccontextmanager
async def _get_session(self):
"""Context manager for aiohttp session."""
if self._session is None:
self._session = aiohttp.ClientSession()
try:
yield self._session
except Exception as e:
logger.error(f"Session error: {e}")
if self._session:
await self._session.close()
self._session = None
raise
async def _wait_for_printer_ready(self, max_wait: int = 300) -> bool:
"""Wait for the printer to be ready.
Args:
max_wait: Maximum time to wait in seconds
Returns:
bool: True if printer is ready, False if timeout
"""
start_time = time.time()
while time.time() - start_time < max_wait:
try:
async with self._get_session() as session:
async with session.get(self.status_url) as response:
if response.status == 200:
data = await response.json()
if data.get('status') == 'ready':
return True
elif data.get('status') == 'busy':
logger.info("Printer is busy, waiting...")
elif response.status == 404:
logger.error(f"Printer status endpoint not found at {self.status_url}")
return False
except aiohttp.ClientError as e:
logger.warning(f"Error checking printer status: {e}")
except Exception as e:
logger.error(f"Unexpected error in _wait_for_printer_ready: {e}")
return False
await asyncio.sleep(1)
logger.error("Timeout waiting for printer to be ready")
return False
async def _send_print_request(self, file_path: Union[str, Path]) -> bool:
"""Send print data to printer API.
Args:
file_path: Path to the binary data file to send to the printer
Returns:
bool: True if request was successful, False otherwise
"""
try:
# Read the binary data from the cache file
with open(file_path, "rb") as f:
print_data = f.read()
# Send the request to the printer API using aiohttp
async with self._get_session() as session:
async with session.post(
self.print_url,
data=print_data,
headers={'Content-Type': 'application/octet-stream'},
timeout=30
) as response:
if response.status == 200:
data = await response.json()
if data.get('message') == 'Print request processed successfully':
return True
logger.error(f"Unexpected success response: {data}")
return False
elif response.status == 404:
logger.error(f"Print endpoint not found at {self.print_url}")
return False
elif response.status == 429:
logger.error("Printer is busy")
return False
else:
data = await response.json()
logger.error(f"Print request failed with status {response.status}: {data.get('message')}")
return False
except aiohttp.ClientError as e:
logger.error(f"Error sending print request: {str(e)}")
return False
except Exception as e:
logger.error(f"Unexpected error in _send_print_request: {e}")
return False
async def print_file(self, file_path: Union[str, Path], label_size: Literal["dk1201", "dk1241"], label_type: Optional[Literal["address_label", "packing_slip"]] = None) -> bool:
"""Print a PDF or PNG file to the label printer.
Args:
file_path: Path to the PDF or PNG file
label_size: Size of label to use ("dk1201" or "dk1241")
label_type: Type of label to use ("address_label" or "packing_slip")
Returns:
bool: True if print was successful, False otherwise
"""
async with self._lock: # Ensure only one print operation at a time
try:
if file_path is None:
logger.error("No file path provided")
return False
file_path = Path(file_path)
if not file_path.exists():
logger.error(f"File not found: {file_path}")
return False
# Wait for printer to be ready
if not await self._wait_for_printer_ready():
logger.error("Printer not ready after waiting")
return False
if file_path.suffix.lower() == '.pdf':
# Convert PDF to images
logger.info(f"Converting PDF {file_path} to images")
images = convert_from_path(file_path)
if not images:
logger.error(f"No images could be extracted from {file_path}")
return False
logger.info(f"Successfully converted PDF to {len(images)} images")
else:
# For PNG files, we can use them directly
images = [Image.open(file_path)]
# Process each page
for i, image in enumerate(images):
logger.info(f"Processing page {i+1} with dimensions {image.size}")
# Resize image based on label size and type
resized_image = image.copy() # Create a copy to work with
# Store the original label size before we modify it
original_label_size = label_size
# Handle resizing based on label size and type
if original_label_size == "dk1241":
if label_type == "packing_slip":
resized_image = resized_image.resize((1164, 1660), Image.Resampling.LANCZOS)
elif label_type == "address_label":
resized_image = resized_image.resize((1660, 1164), Image.Resampling.LANCZOS)
else:
resized_image = resized_image.resize((1164, 1660), Image.Resampling.LANCZOS)
elif original_label_size == "dk1201" and label_type == "address_label":
resized_image = resized_image.resize((991, 306), Image.Resampling.LANCZOS)
# if file path contains address_label, rotate image 90 degrees
if label_type == "address_label":
rotate = "90"
else:
rotate = "0"
# Convert to label format
qlr = BrotherQLRaster("QL-1100")
qlr.exception_on_warning = True
# Get label size based on type
brother_label_size = "29x90" if original_label_size == "dk1201" else "102x152"
converted_image = convert(
qlr=qlr,
images=[resized_image],
label=brother_label_size,
rotate=rotate,
threshold=70.0,
dither=False,
compress=False,
red=False,
dpi_600=False,
#hq=True,
hq=False,
cut=True
)
# Cache the converted binary data
cache_path = self.cache_dir / f"{file_path.stem}_{brother_label_size}_page_{i+1}_converted.bin"
with open(cache_path, "wb") as f:
f.write(converted_image)
# Send to API
if not await self._send_print_request(cache_path):
logger.error(f"Failed to print page {i+1}")
return False
# Wait for printer to be ready before processing next page
if i < len(images) - 1: # Don't wait after the last page
if not await self._wait_for_printer_ready():
logger.error("Printer not ready for next page")
return False
return True
except Exception as e:
logger.error(f"Error printing file {file_path}: {str(e)}")
return False

View File

@ -0,0 +1,182 @@
from typing import Optional, Union, Literal
import os
import aiohttp
import cups
from pathlib import Path
from pdf2image import convert_from_path
from brother_ql.conversion import convert
from brother_ql.raster import BrotherQLRaster
import logging
import asyncio
import time
from datetime import datetime, timedelta
logger = logging.getLogger(__name__)
class PrintService:
def __init__(self, printer_name: Optional[str] = None, printer_api_url: str = "http://localhost:8000/print",
min_print_interval: int = 30):
"""Initialize the print service.
Args:
printer_name: Name of the printer to use. If None, will use default printer.
printer_api_url: URL of the printer API endpoint
min_print_interval: Minimum time in seconds between print requests for label printer
"""
self.printer_name = printer_name
self.printer_api_url = printer_api_url
self.status_url = printer_api_url.replace('/print', '/status')
self.conn = cups.Connection()
self.cache_dir = Path("app/data/cache/prints")
self.cache_dir.mkdir(parents=True, exist_ok=True)
# Rate limiting and coordination
self.min_print_interval = min_print_interval
self._last_print_time = None
self._print_lock = asyncio.Lock()
async def _wait_for_printer_ready(self, max_wait: int = 300) -> bool:
"""Wait for the printer to be ready.
Args:
max_wait: Maximum time to wait in seconds
Returns:
bool: True if printer is ready, False if timeout
"""
start_time = time.time()
while time.time() - start_time < max_wait:
try:
async with aiohttp.ClientSession() as session:
async with session.get(self.status_url) as response:
if response.status == 200:
data = await response.json()
if data.get('status') == 'ready':
return True
except Exception as e:
logger.warning(f"Error checking printer status: {e}")
await asyncio.sleep(1)
logger.error("Timeout waiting for printer to be ready")
return False
async def print_file(self, file_path: Union[str, Path], printer_type: Literal["regular", "label"] = "regular", label_type: Optional[Literal["dk1201", "dk1241"]] = None) -> bool:
"""Print a PDF or PNG file.
Args:
file_path: Path to the PDF or PNG file
printer_type: Type of printer ("regular" or "label")
label_type: Type of label to use ("dk1201" or "dk1241"). Only used when printer_type is "label".
Returns:
bool: True if print was successful, False otherwise
"""
try:
file_path = Path(file_path)
if not file_path.exists():
logger.error(f"File not found: {file_path}")
return False
if printer_type == "regular":
# For regular printers, use CUPS
printer = self.printer_name or self.conn.getDefault()
if not printer:
logger.error("No default printer found")
return False
job_id = self.conn.printFile(
printer,
str(file_path),
f"{file_path.suffix.upper()} Print",
{}
)
logger.info(f"Print job {job_id} submitted to printer {printer}")
return True
else:
# For label printers, we need to coordinate requests
if label_type is None:
logger.error("label_type must be specified when printing to label printer")
return False
# Wait for printer to be ready
if not await self._wait_for_printer_ready():
logger.error("Printer not ready after waiting")
return False
if file_path.suffix.lower() == '.pdf':
# Convert PDF to image first
images = convert_from_path(file_path)
if not images:
logger.error(f"No images could be extracted from {file_path}")
return False
image = images[0] # Only use first page
else:
# For PNG files, we can use them directly
from PIL import Image
image = Image.open(file_path)
# Convert to label format
qlr = BrotherQLRaster("QL-1100")
qlr.exception_on_warning = True
# Get label size based on type
label_size = "29x90" if label_type == "dk1201" else "102x152"
converted_image = convert(
qlr=qlr,
images=[image],
label=label_size,
rotate="0",
threshold=70.0,
dither=False,
compress=False,
red=False,
dpi_600=False,
hq=True,
cut=True
)
# Cache the converted binary data
cache_path = self.cache_dir / f"{file_path.stem}_{label_type}_converted.bin"
with open(cache_path, "wb") as f:
f.write(converted_image)
# Send to API
return await self._send_print_request(cache_path)
except Exception as e:
logger.error(f"Error printing file {file_path}: {str(e)}")
return False
async def _send_print_request(self, file_path: Union[str, Path]) -> bool:
"""Send print data to printer API.
Args:
file_path: Path to the binary data file to send to the printer
Returns:
bool: True if request was successful, False otherwise
"""
try:
# Read the binary data from the cache file
with open(file_path, "rb") as f:
print_data = f.read()
# Send the request to the printer API using aiohttp
async with aiohttp.ClientSession() as session:
async with session.post(
self.printer_api_url,
data=print_data,
timeout=30
) as response:
if response.status == 200:
return True
else:
response_text = await response.text()
logger.error(f"Print request failed with status {response.status}: {response_text}")
return False
except Exception as e:
logger.error(f"Error sending print request: {str(e)}")
return False

View File

@ -0,0 +1,83 @@
from typing import List, Dict
import pandas as pd
from datetime import datetime
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
from weasyprint import HTML
import logging
logger = logging.getLogger(__name__)
class PullSheetService:
def __init__(self):
self.template_dir = Path("app/data/assets/templates")
self.env = Environment(loader=FileSystemLoader(str(self.template_dir)))
self.template = self.env.get_template("pull_sheet.html")
self.output_dir = Path("app/data/cache/tcgplayer/pull_sheets")
self.output_dir.mkdir(parents=True, exist_ok=True)
def generate_pull_sheet_pdf(self, csv_path: str) -> str:
"""Generate a PDF pull sheet from a CSV file.
Args:
csv_path: Path to the CSV file containing pull sheet data
Returns:
Path to the generated PDF file
"""
try:
# Read and process CSV data
items = self._read_and_process_csv(csv_path)
# Prepare template data
template_data = {
'items': items,
'generation_date': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
# Render HTML
html_content = self.template.render(**template_data)
# Generate PDF
pdf_path = self.output_dir / f"pull_sheet_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
HTML(string=html_content).write_pdf(str(pdf_path))
return str(pdf_path)
except Exception as e:
logger.error(f"Error generating pull sheet PDF: {str(e)}")
raise
def _read_and_process_csv(self, csv_path: str) -> List[Dict]:
"""Read and process CSV data using pandas.
Args:
csv_path: Path to the CSV file
Returns:
List of processed items
"""
# Read CSV into pandas DataFrame
df = pd.read_csv(csv_path)
# Filter out the "Orders Contained in Pull Sheet" row
df = df[df['Product Line'] != 'Orders Contained in Pull Sheet:']
# Convert Set Release Date to datetime
df['Set Release Date'] = pd.to_datetime(df['Set Release Date'], format='%m/%d/%Y %H:%M:%S')
# Sort by Set Release Date (descending) and then Product Name (ascending)
df = df.sort_values(['Set Release Date', 'Product Name'], ascending=[False, True])
# Convert to list of dictionaries
items = []
for _, row in df.iterrows():
items.append({
'product_name': row['Product Name'],
'condition': row['Condition'],
'quantity': str(int(row['Quantity'])), # Convert to string for template
'set': row['Set'],
'rarity': row['Rarity']
})
return items

View File

@ -0,0 +1,55 @@
from typing import Optional, Union
import cups
from pathlib import Path
import logging
logger = logging.getLogger(__name__)
class RegularPrinterService:
def __init__(self, printer_name: Optional[str] = None):
"""Initialize the regular printer service.
Args:
printer_name: Name of the printer to use. If None, will use default printer.
"""
self.printer_name = printer_name
self.conn = cups.Connection()
async def print_file(self, file_path: Union[str, Path]) -> bool:
"""Print a PDF or PNG file to the regular printer.
Args:
file_path: Path to the PDF or PNG file
Returns:
bool: True if print was successful, False otherwise
"""
try:
if file_path is None:
logger.error("No file path provided")
return False
file_path = Path(file_path)
if not file_path.exists():
logger.error(f"File not found: {file_path}")
return False
# Get the printer
printer = self.printer_name or self.conn.getDefault()
if not printer:
logger.error("No default printer found")
return False
# Submit the print job
job_id = self.conn.printFile(
printer,
str(file_path),
f"{file_path.suffix.upper()} Print",
{}
)
logger.info(f"Print job {job_id} submitted to printer {printer}")
return True
except Exception as e:
logger.error(f"Error printing file {file_path}: {str(e)}")
return False

312
printer_receiver.py Normal file
View File

@ -0,0 +1,312 @@
#!/usr/bin/env python3
from flask import Flask, request, jsonify
from brother_ql.backends import backend_factory
from brother_ql.backends.helpers import discover, status, send
from brother_ql.raster import BrotherQLRaster
import logging
import sys
import time
from typing import Optional
# Configure logging
logging.basicConfig(
level=logging.DEBUG, # Changed to DEBUG for more detailed logging
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class LabelPrinterReceiver:
def __init__(self, printer_identifier: str = "usb://0x04f9:0x20a7"):
"""Initialize the label printer receiver.
Args:
printer_identifier: USB identifier for the printer
"""
logger.info(f"Initializing LabelPrinterReceiver with printer identifier: {printer_identifier}")
self.printer_identifier = printer_identifier
self.backend = None
self._print_lock = False
self._last_print_time = None
self._print_in_progress = False
self._last_known_state = None
self._last_state_time = None
self._state_cache_timeout = 1.0 # seconds
def connect_printer(self) -> bool:
"""Connect to the printer via USB.
Returns:
bool: True if connection was successful, False otherwise
"""
logger.info("Attempting to connect to printer...")
try:
# Get the backend class from the factory
backend_info = backend_factory('pyusb')
logger.debug(f"Backend info: {backend_info}")
self.backend = backend_info['backend_class'](device_specifier=self.printer_identifier)
# Connect to the printer
self.backend.write = self.backend.write # This triggers the connection
logger.info("Successfully connected to printer")
return True
except Exception as e:
logger.error(f"Error connecting to printer: {e}")
self.backend = None
return False
def is_printer_busy(self) -> bool:
"""Check if the printer is currently busy.
Returns:
bool: True if printer is busy, False otherwise
"""
logger.debug("Checking printer status...")
# Check if we have a recent cached state
if (self._last_known_state is not None and
self._last_state_time is not None and
time.time() - self._last_state_time < self._state_cache_timeout):
logger.debug("Using cached printer state")
return self._last_known_state.get('phase_type') != 'Waiting to receive'
try:
if not self.backend:
logger.warning("No backend connection, attempting to connect...")
if not self.connect_printer():
logger.error("Failed to connect to printer")
return False
# Get actual printer status
logger.debug("Requesting printer status...")
status_info, raw_data = status(printer_identifier=self.printer_identifier)
logger.debug(f"Raw status data: {raw_data}")
logger.debug(f"Parsed status info: {status_info}")
# Cache the state
self._last_known_state = status_info
self._last_state_time = time.time()
# Check for any errors
if status_info.get('errors'):
logger.error(f"Printer errors detected: {status_info['errors']}")
return False
# Check printer phase
phase_type = status_info.get('phase_type')
logger.debug(f"Printer phase type: {phase_type}")
if phase_type == 'Waiting to receive':
logger.info("Printer is ready for next job")
return False
elif phase_type == 'Printing':
logger.info("Printer is currently printing")
return True
else:
logger.info(f"Printer is in unknown phase: {phase_type}")
return True # Assume busy for unknown phases
except Exception as e:
logger.error(f"Error getting printer status: {e}")
# If we get an error, clear the cached state
self._last_known_state = None
self._last_state_time = None
return False
def handle_print_request(self) -> tuple:
"""Handle incoming print requests.
Returns:
tuple: (response message, HTTP status code)
"""
logger.info("Received print request")
if self._print_lock:
logger.warning("Print lock is active, rejecting request")
return {"message": "Another print job is being processed"}, 429
try:
logger.info("Acquiring print lock")
self._print_lock = True
self._print_in_progress = True
self._last_print_time = time.time()
# Get the print data from the request
print_data = request.get_data()
if not print_data:
logger.error("No print data provided in request")
return {"message": "No print data provided"}, 400
logger.info(f"Received print data of size: {len(print_data)} bytes")
# Use the send helper which handles the complete print lifecycle
logger.info("Sending print data to printer...")
result = send(
instructions=print_data,
printer_identifier=self.printer_identifier,
backend_identifier='pyusb',
blocking=True
)
logger.debug(f"Print result: {result}")
# Update cached state with the result
if 'printer_state' in result:
self._last_known_state = result['printer_state']
self._last_state_time = time.time()
if result['outcome'] == 'error':
logger.error(f"Print error: {result.get('printer_state', {}).get('errors', 'Unknown error')}")
return {"message": f"Print error: {result.get('printer_state', {}).get('errors', 'Unknown error')}"}, 500
if not result['did_print']:
logger.warning("Print may not have completed successfully")
return {"message": "Print sent but completion status unclear"}, 200
logger.info("Print completed successfully")
return {"message": "Print request processed successfully"}, 200
except Exception as e:
logger.error(f"Error processing print request: {e}")
# Clear cached state on error
self._last_known_state = None
self._last_state_time = None
return {"message": f"Error: {str(e)}"}, 500
finally:
logger.info("Releasing print lock")
self._print_lock = False
self._print_in_progress = False
def start_server(self, host: str = "0.0.0.0", port: int = 8000):
"""Start the web server to receive print requests.
Args:
host: Host to bind the server to
port: Port to listen on
"""
logger.info(f"Starting print server on {host}:{port}")
app = Flask(__name__)
@app.route('/print', methods=['POST'])
def print_endpoint():
logger.info("Received print request at /print endpoint")
response, status_code = self.handle_print_request()
logger.info(f"Print request completed with status {status_code}: {response}")
return jsonify(response), status_code
@app.route('/status', methods=['GET'])
def status_endpoint():
logger.info("Received status request at /status endpoint")
try:
if not self.backend:
if not self.connect_printer():
logger.error("Failed to connect to printer for status check")
return jsonify({"status": "error", "message": "Could not connect to printer"}), 500
# Get actual printer status with retry logic
max_retries = 3
retry_delay = 1 # seconds
last_error = None
for attempt in range(max_retries):
try:
logger.debug(f"Requesting printer status (attempt {attempt + 1}/{max_retries})...")
status_info, raw_data = status(printer_identifier=self.printer_identifier)
logger.debug(f"Raw status data: {raw_data}")
logger.debug(f"Parsed status info: {status_info}")
# Check for any errors
if status_info.get('errors'):
logger.error(f"Printer errors detected: {status_info['errors']}")
return jsonify({
"status": "error",
"message": f"Printer errors: {status_info['errors']}"
}), 500
# Check printer phase
phase_type = status_info.get('phase_type')
logger.debug(f"Printer phase type: {phase_type}")
# Convert status info to JSON-serializable format
serializable_status = {
'status_type': status_info.get('status_type'),
'phase_type': phase_type,
'model_name': status_info.get('model_name'),
'media_type': status_info.get('media_type'),
'media_width': status_info.get('media_width'),
'media_length': status_info.get('media_length'),
'errors': status_info.get('errors', [])
}
# Add media info if available
if 'identified_media' in status_info:
media = status_info['identified_media']
serializable_status['media'] = {
'identifier': media.identifier,
'tape_size': media.tape_size,
'form_factor': str(media.form_factor),
'color': str(media.color)
}
if phase_type == 'Waiting to receive':
logger.info("Printer status: ready")
return jsonify({
"status": "ready",
"phase": phase_type,
"details": serializable_status
}), 200
elif phase_type == 'Printing':
logger.info("Printer status: busy")
return jsonify({
"status": "busy",
"phase": phase_type,
"details": serializable_status
}), 200
else:
logger.info(f"Printer is in unknown phase: {phase_type}")
return jsonify({
"status": "busy",
"phase": phase_type,
"details": serializable_status
}), 200
except Exception as e:
last_error = e
if "Resource busy" in str(e):
logger.warning(f"Printer is busy, retrying in {retry_delay} seconds...")
time.sleep(retry_delay)
retry_delay *= 2 # Exponential backoff
else:
logger.error(f"Error checking printer status: {e}")
return jsonify({
"status": "error",
"message": str(e)
}), 500
# If we've exhausted all retries
logger.error(f"Failed to get printer status after {max_retries} attempts. Last error: {last_error}")
return jsonify({
"status": "error",
"message": f"Printer is busy and not responding. Last error: {str(last_error)}"
}), 503 # Service Unavailable
except Exception as e:
logger.error(f"Error checking printer status: {e}")
return jsonify({
"status": "error",
"message": str(e)
}), 500
logger.info(f"Print server started successfully on {host}:{port}")
app.run(host=host, port=port)
def main():
# Create and start the printer receiver
logger.info("Starting printer receiver...")
receiver = LabelPrinterReceiver()
receiver.start_server()
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
logger.info("Shutting down print server")
sys.exit(0)