BrickTracker/bricktracker/rebrickable_set.py

226 lines
6.4 KiB
Python
Raw Permalink Normal View History

2025-01-17 11:03:00 +01:00
import logging
from sqlite3 import Row
2025-01-17 11:03:00 +01:00
import traceback
from typing import Any, TYPE_CHECKING
from flask import current_app
from .exceptions import ErrorException, NotFoundException
from .instructions import BrickInstructions
2025-01-17 11:03:00 +01:00
from .rebrickable import Rebrickable
from .rebrickable_image import RebrickableImage
from .record import BrickRecord
from .theme_list import BrickThemeList
2025-01-17 11:03:00 +01:00
if TYPE_CHECKING:
from .socket import BrickSocket
from .theme import BrickTheme
2025-01-17 11:03:00 +01:00
logger = logging.getLogger(__name__)
# A set from Rebrickable
class RebrickableSet(BrickRecord):
2025-01-17 11:03:00 +01:00
socket: 'BrickSocket'
theme: 'BrickTheme'
instructions: list[BrickInstructions]
2025-01-17 11:03:00 +01:00
# Flags
resolve_instructions: bool = True
2025-01-17 11:03:00 +01:00
# Queries
select_query: str = 'rebrickable/set/select'
insert_query: str = 'rebrickable/set/insert'
2025-01-17 11:03:00 +01:00
def __init__(
self,
/,
*,
socket: 'BrickSocket | None' = None,
record: Row | dict[str, Any] | None = None
):
super().__init__()
2025-01-17 11:03:00 +01:00
# Placeholders
self.instructions = []
2025-01-17 11:03:00 +01:00
# Save the socket
if socket is not None:
self.socket = socket
2025-01-17 11:03:00 +01:00
# Ingest the record if it has one
if record is not None:
self.ingest(record)
2025-01-17 11:03:00 +01:00
# Import the set from Rebrickable
def download_rebrickable(self, /) -> None:
# Insert the Rebrickable set to the database
rows, _ = self.insert(
commit=False,
no_defer=True,
override_query=RebrickableSet.insert_query
)
if rows > 0:
if not current_app.config['USE_REMOTE_IMAGES']:
RebrickableImage(self).download()
2025-01-17 11:03:00 +01:00
# Ingest a set
def ingest(self, record: Row | dict[str, Any], /):
super().ingest(record)
2025-01-17 11:03:00 +01:00
# Resolve theme
if not hasattr(self.fields, 'theme_id'):
self.fields.theme_id = 0
2025-01-17 11:03:00 +01:00
self.theme = BrickThemeList().get(self.fields.theme_id)
2025-01-17 11:03:00 +01:00
# Resolve instructions
if self.resolve_instructions:
# Not idead, avoiding cyclic import
from .instructions_list import BrickInstructionsList
2025-01-17 11:03:00 +01:00
if self.fields.set is not None:
self.instructions = BrickInstructionsList().get(
self.fields.set
2025-01-17 11:03:00 +01:00
)
# Load the set from Rebrickable
def load(
self,
data: dict[str, Any],
/,
*,
2025-01-17 11:03:00 +01:00
from_download=False,
) -> bool:
2025-01-17 11:03:00 +01:00
# Reset the progress
self.socket.progress_count = 0
self.socket.progress_total = 2
try:
self.socket.auto_progress(message='Parsing set number')
set = RebrickableSet.parse_number(str(data['set']))
2025-01-17 11:03:00 +01:00
self.socket.auto_progress(
message='Set {set}: loading from Rebrickable'.format(
set=set,
2025-01-17 11:03:00 +01:00
),
)
logger.debug('rebrick.lego.get_set("{set}")'.format(
set=set,
2025-01-17 11:03:00 +01:00
))
Rebrickable[RebrickableSet](
2025-01-17 11:03:00 +01:00
'get_set',
set,
RebrickableSet,
instance=self,
2025-01-17 11:03:00 +01:00
).get()
self.socket.emit('SET_LOADED', self.short(
from_download=from_download
))
2025-01-17 11:03:00 +01:00
if not from_download:
self.socket.complete(
message='Set {set}: loaded from Rebrickable'.format(
set=self.fields.set
2025-01-17 11:03:00 +01:00
)
)
return True
2025-01-17 11:03:00 +01:00
except Exception as e:
self.socket.fail(
message='Could not load the set from Rebrickable: {error}. Data: {data}'.format( # noqa: E501
error=str(e),
data=data,
)
)
if not isinstance(e, (NotFoundException, ErrorException)):
logger.debug(traceback.format_exc())
return False
# Return a short form of the Rebrickable set
def short(self, /, *, from_download: bool = False) -> dict[str, Any]:
return {
'download': from_download,
'image': self.fields.image,
'name': self.fields.name,
'set': self.fields.set,
}
# Compute the url for the set image
def url_for_image(self, /) -> str:
if not current_app.config['USE_REMOTE_IMAGES']:
return RebrickableImage.static_url(
self.fields.set,
'SETS_FOLDER'
)
else:
return self.fields.image
# Compute the url for the rebrickable page
def url_for_rebrickable(self, /) -> str:
if current_app.config['REBRICKABLE_LINKS']:
return self.fields.url
return ''
# Normalize from Rebrickable
@staticmethod
def from_rebrickable(data: dict[str, Any], /, **_) -> dict[str, Any]:
# Extracting version and number
number, _, version = str(data['set_num']).partition('-')
return {
'set': str(data['set_num']),
'number': int(number),
'version': int(version),
'name': str(data['name']),
'year': int(data['year']),
'theme_id': int(data['theme_id']),
'number_of_parts': int(data['num_parts']),
'image': str(data['set_img_url']),
'url': str(data['set_url']),
'last_modified': str(data['last_modified_dt']),
}
2025-01-17 11:03:00 +01:00
# Make sense of the number from the data
@staticmethod
def parse_number(set: str, /) -> str:
number, _, version = set.partition('-')
2025-01-17 11:03:00 +01:00
# Making sure both are integers
if version == '':
version = 1
try:
number = int(number)
except Exception:
raise ErrorException('Number "{number}" is not a number'.format(
number=number,
))
try:
version = int(version)
except Exception:
raise ErrorException('Version "{version}" is not a number'.format(
version=version,
))
# Make sure both are positive
if number < 0:
raise ErrorException('Number "{number}" should be positive'.format(
number=number,
))
if version < 0:
raise ErrorException('Version "{version}" should be positive'.format( # noqa: E501
version=version,
))
return '{number}-{version}'.format(number=number, version=version)