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 *.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 # are written from script.py.mako
# output_encoding = utf-8 # 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] [post_write_hooks]

View File

@ -73,7 +73,6 @@ def upgrade() -> None:
sa.Column('data', sa.JSON(), nullable=True), 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('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.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['card_id'], ['mtgjson_cards.card_id'], ),
sa.PrimaryKeyConstraint('id') sa.PrimaryKeyConstraint('id')
) )
op.create_index(op.f('ix_mtgjson_skus_card_id'), 'mtgjson_skus', ['card_id'], unique=False) 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-13 17:07:45,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,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.scheduler.scheduler_service import SchedulerService
from app.services.data_initialization import DataInitializationService from app.services.data_initialization import DataInitializationService
from datetime import datetime 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 # Configure logging
log_file = "app.log" log_file = "app.log"
if os.path.exists(log_file): if os.path.exists(log_file):
@ -37,7 +43,12 @@ logger.info("Application starting up...")
# Initialize scheduler service # Initialize scheduler service
scheduler_service = SchedulerService() scheduler_service = SchedulerService()
data_init_service = DataInitializationService() 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 @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
# Startup # Startup
@ -47,16 +58,52 @@ async def lifespan(app: FastAPI):
# Initialize TCGPlayer data # Initialize TCGPlayer data
db = SessionLocal() db = SessionLocal()
try: 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") logger.info("TCGPlayer data initialized successfully")
except Exception as e: except Exception as e:
logger.error(f"Failed to initialize TCGPlayer data: {str(e)}") logger.error(f"Failed to initialize TCGPlayer data: {str(e)}")
finally: finally:
db.close() db.close()
# Start the scheduler # Start the scheduler
await scheduler_service.start_scheduled_tasks() #await scheduler_service.start_scheduled_tasks()
await scheduler_service.process_tcgplayer_export(export_type="live", use_cache=True) #await scheduler_service.process_tcgplayer_export(export_type="live", use_cache=True)
logger.info("Scheduler started successfully") logger.info("Scheduler started successfully")
yield yield

View File

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

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.sql import func
from sqlalchemy.orm import relationship
from app.db.database import Base from app.db.database import Base
class MTGJSONSKU(Base): class MTGJSONSKU(Base):
@ -13,9 +12,6 @@ class MTGJSONSKU(Base):
finish = Column(String) finish = Column(String)
language = Column(String) language = Column(String)
printing = 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()) created_at = Column(DateTime(timezone=True), server_default=func.current_timestamp())
updated_at = Column(DateTime(timezone=True), onupdate=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() self.credentials = TCGPlayerCredentials()
def _get_headers(self, method: str) -> Dict[str, str]: def _get_headers(self, method: str, content_type: str = 'application/x-www-form-urlencoded') -> Dict[str, str]:
"""Get headers based on the HTTP method""" """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 = { base_headers = {
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', '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', 'accept-language': 'en-US,en;q=0.8',
'priority': 'u=0, i', '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': '"Not A(Brand";v="8", "Chromium";v="132", "Brave";v="132"',
'sec-ch-ua-mobile': '?0', 'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"', 'sec-ch-ua-platform': '"macOS"',
@ -45,8 +51,7 @@ class BaseTCGPlayerService(BaseExternalService):
if method == 'POST': if method == 'POST':
post_headers = { post_headers = {
'cache-control': 'max-age=0', 'cache-control': 'max-age=0',
'content-type': 'application/x-www-form-urlencoded', 'content-type': content_type
'origin': 'https://store.tcgplayer.com'
} }
base_headers.update(post_headers) 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)