labels and stuff
This commit is contained in:
parent
56c2d1de47
commit
18b32c8514
3
.gitignore
vendored
3
.gitignore
vendored
@ -13,4 +13,5 @@ app/data/cache
|
||||
|
||||
*.log
|
||||
|
||||
.env
|
||||
.env
|
||||
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]
|
||||
|
@ -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)
|
||||
|
29
alembic/versions/2025_04_14_fix_alembic_version.py
Normal file
29
alembic/versions/2025_04_14_fix_alembic_version.py
Normal 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))
|
32
alembic/versions/2025_04_14_fix_foreign_key_issue.py
Normal file
32
alembic/versions/2025_04_14_fix_foreign_key_issue.py
Normal 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
|
33
alembic/versions/2025_04_14_fix_mtgjson_final.py
Normal file
33
alembic/versions/2025_04_14_fix_mtgjson_final.py
Normal 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'])
|
33
alembic/versions/2025_04_14_fix_mtgjson_foreign_key.py
Normal file
33
alembic/versions/2025_04_14_fix_mtgjson_foreign_key.py
Normal 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'])
|
31
alembic/versions/2025_04_14_remove_mtgjson_foreign_key.py
Normal file
31
alembic/versions/2025_04_14_remove_mtgjson_foreign_key.py
Normal 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']
|
||||
)
|
26
alembic/versions/5bf5f87793d7_merge_all_heads.py
Normal file
26
alembic/versions/5bf5f87793d7_merge_all_heads.py
Normal 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
|
26
alembic/versions/d1628d8feb57_merge_heads.py
Normal file
26
alembic/versions/d1628d8feb57_merge_heads.py
Normal 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
20
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
|
||||
|
BIN
app/data/assets/images/ccrcardsaddress.png
Normal file
BIN
app/data/assets/images/ccrcardsaddress.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
70
app/data/assets/templates/address_label_dk1201.html
Normal file
70
app/data/assets/templates/address_label_dk1201.html
Normal 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>
|
105
app/data/assets/templates/address_label_dk1241.html
Normal file
105
app/data/assets/templates/address_label_dk1241.html
Normal 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>
|
136
app/data/assets/templates/pull_sheet.html
Normal file
136
app/data/assets/templates/pull_sheet.html
Normal 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>
|
55
app/main.py
55
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
|
||||
|
@ -1,6 +1,5 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.database import Base
|
||||
|
||||
class MTGJSONCard(Base):
|
||||
@ -41,7 +40,4 @@ class MTGJSONCard(Base):
|
||||
tnt_id = Column(String, nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.current_timestamp())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.current_timestamp())
|
||||
|
||||
# Relationships
|
||||
skus = relationship("MTGJSONSKU", back_populates="card")
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.current_timestamp())
|
@ -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")
|
77
app/services/address_label_service.py
Normal file
77
app/services/address_label_service.py
Normal 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
|
@ -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)
|
||||
|
||||
|
@ -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
|
231
app/services/label_printer_service.py
Normal file
231
app/services/label_printer_service.py
Normal 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
|
182
app/services/print_service.py
Normal file
182
app/services/print_service.py
Normal 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
|
83
app/services/pull_sheet_service.py
Normal file
83
app/services/pull_sheet_service.py
Normal 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
|
55
app/services/regular_printer_service.py
Normal file
55
app/services/regular_printer_service.py
Normal 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
312
printer_receiver.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user