diff --git a/.gitignore b/.gitignore
index a47d56e..f4f4562 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,4 +13,5 @@ app/data/cache
*.log
-.env
\ No newline at end of file
+.env
+alembic.ini
diff --git a/alembic.ini b/alembic.ini
index f175362..4dabebe 100644
--- a/alembic.ini
+++ b/alembic.ini
@@ -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]
diff --git a/alembic/versions/2025_04_13_create_mtgjson_tables.py b/alembic/versions/2025_04_13_create_mtgjson_tables.py
index ab8ef8d..79f059f 100644
--- a/alembic/versions/2025_04_13_create_mtgjson_tables.py
+++ b/alembic/versions/2025_04_13_create_mtgjson_tables.py
@@ -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)
diff --git a/alembic/versions/2025_04_14_fix_alembic_version.py b/alembic/versions/2025_04_14_fix_alembic_version.py
new file mode 100644
index 0000000..42b2dc2
--- /dev/null
+++ b/alembic/versions/2025_04_14_fix_alembic_version.py
@@ -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))
\ No newline at end of file
diff --git a/alembic/versions/2025_04_14_fix_foreign_key_issue.py b/alembic/versions/2025_04_14_fix_foreign_key_issue.py
new file mode 100644
index 0000000..ea6c276
--- /dev/null
+++ b/alembic/versions/2025_04_14_fix_foreign_key_issue.py
@@ -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
\ No newline at end of file
diff --git a/alembic/versions/2025_04_14_fix_mtgjson_final.py b/alembic/versions/2025_04_14_fix_mtgjson_final.py
new file mode 100644
index 0000000..2c3ae94
--- /dev/null
+++ b/alembic/versions/2025_04_14_fix_mtgjson_final.py
@@ -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'])
\ No newline at end of file
diff --git a/alembic/versions/2025_04_14_fix_mtgjson_foreign_key.py b/alembic/versions/2025_04_14_fix_mtgjson_foreign_key.py
new file mode 100644
index 0000000..f5cab6b
--- /dev/null
+++ b/alembic/versions/2025_04_14_fix_mtgjson_foreign_key.py
@@ -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'])
\ No newline at end of file
diff --git a/alembic/versions/2025_04_14_remove_mtgjson_foreign_key.py b/alembic/versions/2025_04_14_remove_mtgjson_foreign_key.py
new file mode 100644
index 0000000..37f6598
--- /dev/null
+++ b/alembic/versions/2025_04_14_remove_mtgjson_foreign_key.py
@@ -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']
+ )
\ No newline at end of file
diff --git a/alembic/versions/5bf5f87793d7_merge_all_heads.py b/alembic/versions/5bf5f87793d7_merge_all_heads.py
new file mode 100644
index 0000000..51f3d84
--- /dev/null
+++ b/alembic/versions/5bf5f87793d7_merge_all_heads.py
@@ -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
diff --git a/alembic/versions/d1628d8feb57_merge_heads.py b/alembic/versions/d1628d8feb57_merge_heads.py
new file mode 100644
index 0000000..91a9192
--- /dev/null
+++ b/alembic/versions/d1628d8feb57_merge_heads.py
@@ -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
diff --git a/app.log b/app.log
index 83bfeda..75fd9da 100644
--- a/app.log
+++ b/app.log
@@ -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
diff --git a/app/data/assets/images/ccrcardsaddress.png b/app/data/assets/images/ccrcardsaddress.png
new file mode 100644
index 0000000..9e95460
Binary files /dev/null and b/app/data/assets/images/ccrcardsaddress.png differ
diff --git a/app/data/assets/templates/address_label_dk1201.html b/app/data/assets/templates/address_label_dk1201.html
new file mode 100644
index 0000000..37051f6
--- /dev/null
+++ b/app/data/assets/templates/address_label_dk1201.html
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
{{ recipient_name }}
+
{{ address_line1 }}
+ {% if address_line2 %}
{{ address_line2 }}
{% endif %}
+
{{ city }}, {{ state }} {{ zip_code }}
+
+
+
+
\ No newline at end of file
diff --git a/app/data/assets/templates/address_label_dk1241.html b/app/data/assets/templates/address_label_dk1241.html
new file mode 100644
index 0000000..0ad9ab8
--- /dev/null
+++ b/app/data/assets/templates/address_label_dk1241.html
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+

+
+
+ PLACE
STAMP
HERE
+
+
+
+
{{ recipient_name }}
+
{{ address_line1 }}
+ {% if address_line2 %}
{{ address_line2 }}
{% endif %}
+
{{ city }}, {{ state }} {{ zip_code }}
+
+
+
+
diff --git a/app/data/assets/templates/pull_sheet.html b/app/data/assets/templates/pull_sheet.html
new file mode 100644
index 0000000..68ca5ab
--- /dev/null
+++ b/app/data/assets/templates/pull_sheet.html
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Product Name |
+ Condition |
+ Qty |
+ Set |
+ Rarity |
+
+
+
+ {% for item in items %}
+
+ {{ item.product_name }} |
+ {{ item.condition }} |
+ {{ item.quantity }} |
+ {{ item.set }} |
+ {{ item.rarity }} |
+
+ {% endfor %}
+
+
+
+
\ No newline at end of file
diff --git a/app/main.py b/app/main.py
index 33eec5a..92b848e 100644
--- a/app/main.py
+++ b/app/main.py
@@ -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
diff --git a/app/models/mtgjson_card.py b/app/models/mtgjson_card.py
index ead70af..58f27d1 100644
--- a/app/models/mtgjson_card.py
+++ b/app/models/mtgjson_card.py
@@ -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")
\ No newline at end of file
+ updated_at = Column(DateTime(timezone=True), onupdate=func.current_timestamp())
\ No newline at end of file
diff --git a/app/models/mtgjson_sku.py b/app/models/mtgjson_sku.py
index 4997651..b6ea06a 100644
--- a/app/models/mtgjson_sku.py
+++ b/app/models/mtgjson_sku.py
@@ -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")
\ No newline at end of file
diff --git a/app/services/address_label_service.py b/app/services/address_label_service.py
new file mode 100644
index 0000000..788667d
--- /dev/null
+++ b/app/services/address_label_service.py
@@ -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
\ No newline at end of file
diff --git a/app/services/external_api/tcgplayer/base_tcgplayer_service.py b/app/services/external_api/tcgplayer/base_tcgplayer_service.py
index 4d97a98..2b31311 100644
--- a/app/services/external_api/tcgplayer/base_tcgplayer_service.py
+++ b/app/services/external_api/tcgplayer/base_tcgplayer_service.py
@@ -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)
diff --git a/app/services/external_api/tcgplayer/order_management_service.py b/app/services/external_api/tcgplayer/order_management_service.py
new file mode 100644
index 0000000..7a67d42
--- /dev/null
+++ b/app/services/external_api/tcgplayer/order_management_service.py
@@ -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
\ No newline at end of file
diff --git a/app/services/label_printer_service.py b/app/services/label_printer_service.py
new file mode 100644
index 0000000..d703ddf
--- /dev/null
+++ b/app/services/label_printer_service.py
@@ -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
\ No newline at end of file
diff --git a/app/services/print_service.py b/app/services/print_service.py
new file mode 100644
index 0000000..6ac5f8a
--- /dev/null
+++ b/app/services/print_service.py
@@ -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
diff --git a/app/services/pull_sheet_service.py b/app/services/pull_sheet_service.py
new file mode 100644
index 0000000..8b7a89b
--- /dev/null
+++ b/app/services/pull_sheet_service.py
@@ -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
\ No newline at end of file
diff --git a/app/services/regular_printer_service.py b/app/services/regular_printer_service.py
new file mode 100644
index 0000000..22e57e9
--- /dev/null
+++ b/app/services/regular_printer_service.py
@@ -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
\ No newline at end of file
diff --git a/printer_receiver.py b/printer_receiver.py
new file mode 100644
index 0000000..8db0274
--- /dev/null
+++ b/printer_receiver.py
@@ -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)
\ No newline at end of file