Don't store complex objects in Flash config that could mask existing config items, rather store the values and handle the actual list of conf differently

This commit is contained in:
Gregoo 2025-01-20 15:20:07 +01:00
parent 3712356caa
commit e232e2ab7f
37 changed files with 126 additions and 118 deletions

12
app.py
View File

@ -20,19 +20,19 @@ setup_app(app)
# Create the socket # Create the socket
s = BrickSocket( s = BrickSocket(
app, app,
threaded=not app.config['NO_THREADED_SOCKET'].value, threaded=not app.config['NO_THREADED_SOCKET'],
) )
if __name__ == '__main__': if __name__ == '__main__':
# Run the application # Run the application
logger.info('Starting BrickTracker on {host}:{port}'.format( logger.info('Starting BrickTracker on {host}:{port}'.format(
host=app.config['HOST'].value, host=app.config['HOST'],
port=app.config['PORT'].value, port=app.config['PORT'],
)) ))
s.socket.run( s.socket.run(
app, app,
host=app.config['HOST'].value, host=app.config['HOST'],
debug=app.config['DEBUG'].value, debug=app.config['DEBUG'],
port=app.config['PORT'].value, port=app.config['PORT'],
) )

View File

@ -28,7 +28,7 @@ def setup_app(app: Flask) -> None:
BrickConfigurationList(app) BrickConfigurationList(app)
# Set the logging level # Set the logging level
if app.config['DEBUG'].value: if app.config['DEBUG']:
logging.basicConfig( logging.basicConfig(
stream=sys.stdout, stream=sys.stdout,
level=logging.DEBUG, level=logging.DEBUG,
@ -90,7 +90,7 @@ def setup_app(app: Flask) -> None:
g.request_time = request_time g.request_time = request_time
# Register the timezone # Register the timezone
g.timezone = ZoneInfo(current_app.config['TIMEZONE'].value) g.timezone = ZoneInfo(current_app.config['TIMEZONE'])
# Version # Version
g.version = __version__ g.version = __version__

View File

@ -69,6 +69,7 @@ class BrickConfiguration(object):
# Remove static prefix # Remove static prefix
value = value.removeprefix('static/') value = value.removeprefix('static/')
# Type casting
if self.cast is not None: if self.cast is not None:
self.value = self.cast(value) self.value = self.cast(value)
else: else:

View File

@ -1,46 +1,60 @@
import logging
from typing import Generator from typing import Generator
from flask import current_app, Flask from flask import Flask
from .config import CONFIG from .config import CONFIG
from .configuration import BrickConfiguration from .configuration import BrickConfiguration
from .exceptions import ConfigurationMissingException from .exceptions import ConfigurationMissingException
logger = logging.getLogger(__name__)
# Application configuration # Application configuration
class BrickConfigurationList(object): class BrickConfigurationList(object):
app: Flask app: Flask
configurations: dict[str, BrickConfiguration]
# Load configuration # Load configuration
def __init__(self, app: Flask, /): def __init__(self, app: Flask, /):
self.app = app self.app = app
# Load the configurations only there is none already loaded
configurations = getattr(self, 'configurations', None)
if configurations is None:
logger.info('Loading configuration variables')
BrickConfigurationList.configurations = {}
# Process all configuration items # Process all configuration items
for config in CONFIG: for config in CONFIG:
item = BrickConfiguration(**config) item = BrickConfiguration(**config)
self.app.config[item.name] = item
# Store in the list
BrickConfigurationList.configurations[item.name] = item
# Only store the value in the app to avoid breaking any
# existing variables
self.app.config[item.name] = item.value
# Check whether a str configuration is set # Check whether a str configuration is set
@staticmethod @staticmethod
def error_unless_is_set(name: str): def error_unless_is_set(name: str):
config: BrickConfiguration = current_app.config[name] configuration = BrickConfigurationList.configurations[name]
if config.value is None or config.value == '': if configuration.value is None or configuration.value == '':
raise ConfigurationMissingException( raise ConfigurationMissingException(
'{name} must be defined (using the {environ} environment variable)'.format( # noqa: E501 '{name} must be defined (using the {environ} environment variable)'.format( # noqa: E501
name=config.name, name=name,
environ=config.env_name environ=configuration.env_name
), ),
) )
# Get all the configuration items from the app config # Get all the configuration items from the app config
@staticmethod @staticmethod
def list() -> Generator[BrickConfiguration, None, None]: def list() -> Generator[BrickConfiguration, None, None]:
keys = list(current_app.config.keys()) keys = sorted(BrickConfigurationList.configurations.keys())
keys.sort()
for name in keys: for name in keys:
config = current_app.config[name] yield BrickConfigurationList.configurations[name]
if isinstance(config, BrickConfiguration):
yield config

View File

@ -39,7 +39,7 @@ class BrickInstructions(object):
# Store the name and extension, check if extension is allowed # Store the name and extension, check if extension is allowed
self.name, self.extension = os.path.splitext(self.filename) self.name, self.extension = os.path.splitext(self.filename)
self.extension = self.extension.lower() self.extension = self.extension.lower()
self.allowed = self.extension in current_app.config['INSTRUCTIONS_ALLOWED_EXTENSIONS'].value # noqa: E501 self.allowed = self.extension in current_app.config['INSTRUCTIONS_ALLOWED_EXTENSIONS'] # noqa: E501
# Placeholder # Placeholder
self.brickset = None self.brickset = None
@ -67,7 +67,7 @@ class BrickInstructions(object):
# Display the time in a human format # Display the time in a human format
def human_time(self) -> str: def human_time(self) -> str:
return self.mtime.astimezone(g.timezone).strftime( return self.mtime.astimezone(g.timezone).strftime(
current_app.config['FILE_DATETIME_FORMAT'].value current_app.config['FILE_DATETIME_FORMAT']
) )
# Compute the path of an instruction file # Compute the path of an instruction file
@ -77,7 +77,7 @@ class BrickInstructions(object):
return os.path.join( return os.path.join(
current_app.static_folder, # type: ignore current_app.static_folder, # type: ignore
current_app.config['INSTRUCTIONS_FOLDER'].value, current_app.config['INSTRUCTIONS_FOLDER'],
filename filename
) )
@ -118,7 +118,7 @@ class BrickInstructions(object):
if not self.allowed: if not self.allowed:
return '' return ''
folder: str = current_app.config['INSTRUCTIONS_FOLDER'].value folder: str = current_app.config['INSTRUCTIONS_FOLDER']
# Compute the path # Compute the path
path = os.path.join(folder, self.filename) path = os.path.join(folder, self.filename)

View File

@ -36,7 +36,7 @@ class BrickInstructionsList(object):
# Make a folder relative to static # Make a folder relative to static
folder: str = os.path.join( folder: str = os.path.join(
current_app.static_folder, # type: ignore current_app.static_folder, # type: ignore
current_app.config['INSTRUCTIONS_FOLDER'].value, current_app.config['INSTRUCTIONS_FOLDER'],
) )
for file in os.scandir(folder): for file in os.scandir(folder):
@ -68,9 +68,8 @@ class BrickInstructionsList(object):
for brickset in BrickSetList().generic().records: for brickset in BrickSetList().generic().records:
bricksets[brickset.fields.set_num] = brickset bricksets[brickset.fields.set_num] = brickset
# Return the files
for instruction in self.all.values():
# Inject the brickset if it exists # Inject the brickset if it exists
for instruction in self.all.values():
if ( if (
instruction.allowed and instruction.allowed and
instruction.number is not None and instruction.number is not None and

View File

@ -12,7 +12,7 @@ class LoginManager(object):
def __init__(self, app: Flask, /): def __init__(self, app: Flask, /):
# Setup basic authentication # Setup basic authentication
app.secret_key = app.config['AUTHENTICATION_KEY'].value app.secret_key = app.config['AUTHENTICATION_KEY']
manager = login_manager.LoginManager() manager = login_manager.LoginManager()
manager.login_view = 'login.login' # type: ignore manager.login_view = 'login.login' # type: ignore
@ -23,11 +23,11 @@ class LoginManager(object):
def user_loader(*arg) -> LoginManager.User: def user_loader(*arg) -> LoginManager.User:
return self.User( return self.User(
'admin', 'admin',
app.config['AUTHENTICATION_PASSWORD'].value app.config['AUTHENTICATION_PASSWORD']
) )
# If the password is unset, globally disable # If the password is unset, globally disable
app.config['LOGIN_DISABLED'] = app.config['AUTHENTICATION_PASSWORD'].value == '' # noqa: E501 app.config['LOGIN_DISABLED'] = app.config['AUTHENTICATION_PASSWORD'] == '' # noqa: E501
# Tells whether the user is authenticated, meaning: # Tells whether the user is authenticated, meaning:
# - Authentication disabled # - Authentication disabled

View File

@ -119,7 +119,7 @@ class BrickMinifigure(BrickRecord):
# Compute the url for minifigure part image # Compute the url for minifigure part image
def url_for_image(self, /) -> str: def url_for_image(self, /) -> str:
if not current_app.config['USE_REMOTE_IMAGES'].value: if not current_app.config['USE_REMOTE_IMAGES']:
if self.fields.set_img_url is None: if self.fields.set_img_url is None:
file = RebrickableImage.nil_minifigure_name() file = RebrickableImage.nil_minifigure_name()
else: else:
@ -128,15 +128,15 @@ class BrickMinifigure(BrickRecord):
return RebrickableImage.static_url(file, 'MINIFIGURES_FOLDER') return RebrickableImage.static_url(file, 'MINIFIGURES_FOLDER')
else: else:
if self.fields.set_img_url is None: if self.fields.set_img_url is None:
return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE'].value # noqa: E501 return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE']
else: else:
return self.fields.set_img_url return self.fields.set_img_url
# Compute the url for the rebrickable page # Compute the url for the rebrickable page
def url_for_rebrickable(self, /) -> str: def url_for_rebrickable(self, /) -> str:
if current_app.config['REBRICKABLE_LINKS'].value: if current_app.config['REBRICKABLE_LINKS']:
try: try:
return current_app.config['REBRICKABLE_LINK_MINIFIGURE_PATTERN'].value.format( # noqa: E501 return current_app.config['REBRICKABLE_LINK_MINIFIGURE_PATTERN'].format( # noqa: E501
number=self.fields.fig_num.lower(), number=self.fields.fig_num.lower(),
) )
except Exception: except Exception:

View File

@ -27,7 +27,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
self.brickset = None self.brickset = None
# Store the order for this list # Store the order for this list
self.order = current_app.config['MINIFIGURES_DEFAULT_ORDER'].value self.order = current_app.config['MINIFIGURES_DEFAULT_ORDER']
# Load all minifigures # Load all minifigures
def all(self, /) -> Self: def all(self, /) -> Self:
@ -44,7 +44,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
# Last added minifigure # Last added minifigure
def last(self, /, limit: int = 6) -> Self: def last(self, /, limit: int = 6) -> Self:
# Randomize # Randomize
if current_app.config['RANDOM'].value: if current_app.config['RANDOM']:
order = 'RANDOM()' order = 'RANDOM()'
else: else:
order = 'minifigures.rowid DESC' order = 'minifigures.rowid DESC'

View File

@ -190,9 +190,9 @@ class BrickPart(BrickRecord):
# Compute the url for the bricklink page # Compute the url for the bricklink page
def url_for_bricklink(self, /) -> str: def url_for_bricklink(self, /) -> str:
if current_app.config['BRICKLINK_LINKS'].value: if current_app.config['BRICKLINK_LINKS']:
try: try:
return current_app.config['BRICKLINK_LINK_PART_PATTERN'].value.format( # noqa: E501 return current_app.config['BRICKLINK_LINK_PART_PATTERN'].format( # noqa: E501
number=self.fields.part_num, number=self.fields.part_num,
) )
except Exception: except Exception:
@ -202,7 +202,7 @@ class BrickPart(BrickRecord):
# Compute the url for the part image # Compute the url for the part image
def url_for_image(self, /) -> str: def url_for_image(self, /) -> str:
if not current_app.config['USE_REMOTE_IMAGES'].value: if not current_app.config['USE_REMOTE_IMAGES']:
if self.fields.part_img_url is None: if self.fields.part_img_url is None:
file = RebrickableImage.nil_name() file = RebrickableImage.nil_name()
else: else:
@ -211,7 +211,7 @@ class BrickPart(BrickRecord):
return RebrickableImage.static_url(file, 'PARTS_FOLDER') return RebrickableImage.static_url(file, 'PARTS_FOLDER')
else: else:
if self.fields.part_img_url is None: if self.fields.part_img_url is None:
return current_app.config['REBRICKABLE_IMAGE_NIL'].value return current_app.config['REBRICKABLE_IMAGE_NIL']
else: else:
return self.fields.part_img_url return self.fields.part_img_url
@ -234,9 +234,9 @@ class BrickPart(BrickRecord):
# Compute the url for the rebrickable page # Compute the url for the rebrickable page
def url_for_rebrickable(self, /) -> str: def url_for_rebrickable(self, /) -> str:
if current_app.config['REBRICKABLE_LINKS'].value: if current_app.config['REBRICKABLE_LINKS']:
try: try:
return current_app.config['REBRICKABLE_LINK_PART_PATTERN'].value.format( # noqa: E501 return current_app.config['REBRICKABLE_LINK_PART_PATTERN'].format( # noqa: E501
number=self.fields.part_num, number=self.fields.part_num,
color=self.fields.color_id, color=self.fields.color_id,
) )

View File

@ -30,7 +30,7 @@ class BrickPartList(BrickRecordList[BrickPart]):
self.minifigure = None self.minifigure = None
# Store the order for this list # Store the order for this list
self.order = current_app.config['PARTS_DEFAULT_ORDER'].value self.order = current_app.config['PARTS_DEFAULT_ORDER']
# Load all parts # Load all parts
def all(self, /) -> Self: def all(self, /) -> Self:
@ -63,10 +63,7 @@ class BrickPartList(BrickRecordList[BrickPart]):
record=record, record=record,
) )
if ( if current_app.config['SKIP_SPARE_PARTS'] and part.fields.is_spare:
current_app.config['SKIP_SPARE_PARTS'].value and
part.fields.is_spare
):
continue continue
self.records.append(part) self.records.append(part)
@ -92,10 +89,7 @@ class BrickPartList(BrickRecordList[BrickPart]):
record=record, record=record,
) )
if ( if current_app.config['SKIP_SPARE_PARTS'] and part.fields.is_spare:
current_app.config['SKIP_SPARE_PARTS'].value and
part.fields.is_spare
):
continue continue
self.records.append(part) self.records.append(part)

View File

@ -77,7 +77,7 @@ class Rebrickable(Generic[T]):
# Bootstrap a first set of parameters # Bootstrap a first set of parameters
parameters: dict[str, Any] | None = { parameters: dict[str, Any] | None = {
'page_size': current_app.config['REBRICKABLE_PAGE_SIZE'].value, 'page_size': current_app.config['REBRICKABLE_PAGE_SIZE'],
} }
# Read all pages # Read all pages
@ -115,7 +115,7 @@ class Rebrickable(Generic[T]):
# Load from the API # Load from the API
def load(self, /, parameters: dict[str, Any] = {}) -> dict[str, Any]: def load(self, /, parameters: dict[str, Any] = {}) -> dict[str, Any]:
# Inject the API key # Inject the API key
parameters['api_key'] = current_app.config['REBRICKABLE_API_KEY'].value, # noqa: E501 parameters['api_key'] = current_app.config['REBRICKABLE_API_KEY']
try: try:
return json.loads( return json.loads(

View File

@ -70,12 +70,12 @@ class RebrickableImage(object):
# Return the folder depending on the objects provided # Return the folder depending on the objects provided
def folder(self, /) -> str: def folder(self, /) -> str:
if self.part is not None: if self.part is not None:
return current_app.config['PARTS_FOLDER'].value return current_app.config['PARTS_FOLDER']
if self.minifigure is not None: if self.minifigure is not None:
return current_app.config['MINIFIGURES_FOLDER'].value return current_app.config['MINIFIGURES_FOLDER']
return current_app.config['SETS_FOLDER'].value return current_app.config['SETS_FOLDER']
# Return the id depending on the objects provided # Return the id depending on the objects provided
def id(self, /) -> str: def id(self, /) -> str:
@ -105,13 +105,13 @@ class RebrickableImage(object):
def url(self, /) -> str: def url(self, /) -> str:
if self.part is not None: if self.part is not None:
if self.part.fields.part_img_url is None: if self.part.fields.part_img_url is None:
return current_app.config['REBRICKABLE_IMAGE_NIL'].value return current_app.config['REBRICKABLE_IMAGE_NIL']
else: else:
return self.part.fields.part_img_url return self.part.fields.part_img_url
if self.minifigure is not None: if self.minifigure is not None:
if self.minifigure.fields.set_img_url is None: if self.minifigure.fields.set_img_url is None:
return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE'].value # noqa: E501 return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE']
else: else:
return self.minifigure.fields.set_img_url return self.minifigure.fields.set_img_url
@ -122,7 +122,7 @@ class RebrickableImage(object):
def nil_name() -> str: def nil_name() -> str:
filename, _ = os.path.splitext( filename, _ = os.path.splitext(
os.path.basename( os.path.basename(
urlparse(current_app.config['REBRICKABLE_IMAGE_NIL'].value).path # noqa: E501 urlparse(current_app.config['REBRICKABLE_IMAGE_NIL']).path
) )
) )
@ -133,7 +133,7 @@ class RebrickableImage(object):
def nil_minifigure_name() -> str: def nil_minifigure_name() -> str:
filename, _ = os.path.splitext( filename, _ = os.path.splitext(
os.path.basename( os.path.basename(
urlparse(current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE'].value).path # noqa: E501 urlparse(current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE']).path # noqa: E501
) )
) )
@ -142,7 +142,7 @@ class RebrickableImage(object):
# Return the static URL for an image given a name and folder # Return the static URL for an image given a name and folder
@staticmethod @staticmethod
def static_url(name: str, folder_name: str) -> str: def static_url(name: str, folder_name: str) -> str:
folder: str = current_app.config[folder_name].value folder: str = current_app.config[folder_name]
# /!\ Everything is saved as .jpg, even if it came from a .png # /!\ Everything is saved as .jpg, even if it came from a .png
# not changing this behaviour. # not changing this behaviour.

View File

@ -71,7 +71,7 @@ class RebrickableMinifigures(object):
) )
) )
if not current_app.config['USE_REMOTE_IMAGES'].value: if not current_app.config['USE_REMOTE_IMAGES']:
RebrickableImage( RebrickableImage(
self.brickset, self.brickset,
minifigure=minifigure minifigure=minifigure

View File

@ -76,7 +76,7 @@ class RebrickableParts(object):
for index, part in enumerate(inventory): for index, part in enumerate(inventory):
# Skip spare parts # Skip spare parts
if ( if (
current_app.config['SKIP_SPARE_PARTS'].value and current_app.config['SKIP_SPARE_PARTS'] and
part.fields.is_spare part.fields.is_spare
): ):
continue continue
@ -104,7 +104,7 @@ class RebrickableParts(object):
) )
) )
if not current_app.config['USE_REMOTE_IMAGES'].value: if not current_app.config['USE_REMOTE_IMAGES']:
RebrickableImage( RebrickableImage(
self.brickset, self.brickset,
minifigure=self.minifigure, minifigure=self.minifigure,

View File

@ -55,7 +55,7 @@ class RebrickableSet(object):
# Insert into database # Insert into database
brickset.insert(commit=False) brickset.insert(commit=False)
if not current_app.config['USE_REMOTE_IMAGES'].value: if not current_app.config['USE_REMOTE_IMAGES']:
RebrickableImage(brickset).download() RebrickableImage(brickset).download()
# Load the inventory # Load the inventory
@ -210,5 +210,5 @@ class RebrickableSet(object):
# Insert into database # Insert into database
brickwish.insert() brickwish.insert()
if not current_app.config['USE_REMOTE_IMAGES'].value: if not current_app.config['USE_REMOTE_IMAGES']:
RebrickableImage(brickwish).download() RebrickableImage(brickwish).download()

View File

@ -33,7 +33,7 @@ class BrickRetiredList(object):
# Try to read the themes from a CSV file # Try to read the themes from a CSV file
try: try:
with open(current_app.config['RETIRED_SETS_PATH'].value, newline='') as themes_file: # noqa: E501 with open(current_app.config['RETIRED_SETS_PATH'], newline='') as themes_file: # noqa: E501
themes_reader = csv.reader(themes_file) themes_reader = csv.reader(themes_file)
# Ignore the header # Ignore the header
@ -44,7 +44,7 @@ class BrickRetiredList(object):
BrickRetiredList.retired[retired.number] = retired BrickRetiredList.retired[retired.number] = retired
# File stats # File stats
stat = os.stat(current_app.config['RETIRED_SETS_PATH'].value) stat = os.stat(current_app.config['RETIRED_SETS_PATH'])
BrickRetiredList.size = stat.st_size BrickRetiredList.size = stat.st_size
BrickRetiredList.mtime = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc) # noqa: E501 BrickRetiredList.mtime = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc) # noqa: E501
@ -79,7 +79,7 @@ class BrickRetiredList(object):
def human_time(self) -> str: def human_time(self) -> str:
if self.mtime is not None: if self.mtime is not None:
return self.mtime.astimezone(g.timezone).strftime( return self.mtime.astimezone(g.timezone).strftime(
current_app.config['FILE_DATETIME_FORMAT'].value current_app.config['FILE_DATETIME_FORMAT']
) )
else: else:
return '' return ''
@ -88,7 +88,7 @@ class BrickRetiredList(object):
@staticmethod @staticmethod
def update() -> None: def update() -> None:
response = requests.get( response = requests.get(
current_app.config['RETIRED_SETS_FILE_URL'].value, current_app.config['RETIRED_SETS_FILE_URL'],
stream=True, stream=True,
) )
@ -99,7 +99,7 @@ class BrickRetiredList(object):
content = gzip.GzipFile(fileobj=response.raw) content = gzip.GzipFile(fileobj=response.raw)
with open(current_app.config['RETIRED_SETS_PATH'].value, 'wb') as f: with open(current_app.config['RETIRED_SETS_PATH'], 'wb') as f:
copyfileobj(content, f) copyfileobj(content, f)
logger.info('Retired sets list updated') logger.info('Retired sets list updated')

View File

@ -157,7 +157,7 @@ class BrickSet(BrickRecord):
# Compute the url for the set image # Compute the url for the set image
def url_for_image(self, /) -> str: def url_for_image(self, /) -> str:
if not current_app.config['USE_REMOTE_IMAGES'].value: if not current_app.config['USE_REMOTE_IMAGES']:
return RebrickableImage.static_url( return RebrickableImage.static_url(
self.fields.set_num, self.fields.set_num,
'SETS_FOLDER' 'SETS_FOLDER'
@ -182,9 +182,9 @@ class BrickSet(BrickRecord):
# Compute the url for the rebrickable page # Compute the url for the rebrickable page
def url_for_rebrickable(self, /) -> str: def url_for_rebrickable(self, /) -> str:
if current_app.config['REBRICKABLE_LINKS'].value: if current_app.config['REBRICKABLE_LINKS']:
try: try:
return current_app.config['REBRICKABLE_LINK_SET_PATTERN'].value.format( # noqa: E501 return current_app.config['REBRICKABLE_LINK_SET_PATTERN'].format( # noqa: E501
number=self.fields.set_num.lower(), number=self.fields.set_num.lower(),
) )
except Exception: except Exception:

View File

@ -26,7 +26,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
self.themes = [] self.themes = []
# Store the order for this list # Store the order for this list
self.order = current_app.config['SETS_DEFAULT_ORDER'].value self.order = current_app.config['SETS_DEFAULT_ORDER']
# All the sets # All the sets
def all(self, /) -> Self: def all(self, /) -> Self:
@ -60,7 +60,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
# Last added sets # Last added sets
def last(self, /, limit: int = 6) -> Self: def last(self, /, limit: int = 6) -> Self:
# Randomize # Randomize
if current_app.config['RANDOM'].value: if current_app.config['RANDOM']:
order = 'RANDOM()' order = 'RANDOM()'
else: else:
order = 'sets.rowid DESC' order = 'sets.rowid DESC'

View File

@ -56,19 +56,19 @@ class BrickSocket(object):
# Compute the namespace # Compute the namespace
self.namespace = '/{namespace}'.format( self.namespace = '/{namespace}'.format(
namespace=app.config['SOCKET_NAMESPACE'].value namespace=app.config['SOCKET_NAMESPACE']
) )
# Inject CORS if a domain is defined # Inject CORS if a domain is defined
if app.config['DOMAIN_NAME'].value != '': if app.config['DOMAIN_NAME'] != '':
kwargs['cors_allowed_origins'] = app.config['DOMAIN_NAME'].value kwargs['cors_allowed_origins'] = app.config['DOMAIN_NAME']
# Instantiate the socket # Instantiate the socket
self.socket = SocketIO( self.socket = SocketIO(
self.app, self.app,
*args, *args,
**kwargs, **kwargs,
path=app.config['SOCKET_PATH'].value, path=app.config['SOCKET_PATH'],
async_mode='eventlet', async_mode='eventlet',
) )

View File

@ -37,7 +37,7 @@ class BrickSQL(object):
logger.debug('SQLite3: connect') logger.debug('SQLite3: connect')
self.connection = sqlite3.connect( self.connection = sqlite3.connect(
current_app.config['DATABASE_PATH'].value current_app.config['DATABASE_PATH']
) )
# Setup the row factory to get pseudo-dicts rather than tuples # Setup the row factory to get pseudo-dicts rather than tuples
@ -249,7 +249,7 @@ class BrickSQL(object):
# Delete the database # Delete the database
@staticmethod @staticmethod
def delete() -> None: def delete() -> None:
os.remove(current_app.config['DATABASE_PATH'].value) os.remove(current_app.config['DATABASE_PATH'])
# Info # Info
logger.info('The database has been deleted') logger.info('The database has been deleted')
@ -292,7 +292,7 @@ class BrickSQL(object):
# Replace the database with a new file # Replace the database with a new file
@staticmethod @staticmethod
def upload(file: FileStorage, /) -> None: def upload(file: FileStorage, /) -> None:
file.save(current_app.config['DATABASE_PATH'].value) file.save(current_app.config['DATABASE_PATH'])
# Info # Info
logger.info('The database has been imported using file {file}'.format( logger.info('The database has been imported using file {file}'.format(

View File

@ -33,7 +33,7 @@ class BrickThemeList(object):
# Try to read the themes from a CSV file # Try to read the themes from a CSV file
try: try:
with open(current_app.config['THEMES_PATH'].value, newline='') as themes_file: # noqa: E501 with open(current_app.config['THEMES_PATH'], newline='') as themes_file: # noqa: E501
themes_reader = csv.reader(themes_file) themes_reader = csv.reader(themes_file)
# Ignore the header # Ignore the header
@ -44,7 +44,7 @@ class BrickThemeList(object):
BrickThemeList.themes[theme.id] = theme BrickThemeList.themes[theme.id] = theme
# File stats # File stats
stat = os.stat(current_app.config['THEMES_PATH'].value) stat = os.stat(current_app.config['THEMES_PATH'])
BrickThemeList.size = stat.st_size BrickThemeList.size = stat.st_size
BrickThemeList.mtime = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc) # noqa: E501 BrickThemeList.mtime = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc) # noqa: E501
@ -78,7 +78,7 @@ class BrickThemeList(object):
def human_time(self) -> str: def human_time(self) -> str:
if self.mtime is not None: if self.mtime is not None:
return self.mtime.astimezone(g.timezone).strftime( return self.mtime.astimezone(g.timezone).strftime(
current_app.config['FILE_DATETIME_FORMAT'].value current_app.config['FILE_DATETIME_FORMAT']
) )
else: else:
return '' return ''
@ -87,7 +87,7 @@ class BrickThemeList(object):
@staticmethod @staticmethod
def update() -> None: def update() -> None:
response = requests.get( response = requests.get(
current_app.config['THEMES_FILE_URL'].value, current_app.config['THEMES_FILE_URL'],
stream=True, stream=True,
) )
@ -98,7 +98,7 @@ class BrickThemeList(object):
content = gzip.GzipFile(fileobj=response.raw) content = gzip.GzipFile(fileobj=response.raw)
with open(current_app.config['THEMES_PATH'].value, 'wb') as f: with open(current_app.config['THEMES_PATH'], 'wb') as f:
copyfileobj(content, f) copyfileobj(content, f)
logger.info('Theme list updated') logger.info('Theme list updated')

View File

@ -17,8 +17,8 @@ def add() -> str:
return render_template( return render_template(
'add.html', 'add.html',
path=current_app.config['SOCKET_PATH'].value, path=current_app.config['SOCKET_PATH'],
namespace=current_app.config['SOCKET_NAMESPACE'].value, namespace=current_app.config['SOCKET_NAMESPACE'],
messages=MESSAGES messages=MESSAGES
) )
@ -32,7 +32,7 @@ def bulk() -> str:
return render_template( return render_template(
'bulk.html', 'bulk.html',
path=current_app.config['SOCKET_PATH'].value, path=current_app.config['SOCKET_PATH'],
namespace=current_app.config['SOCKET_NAMESPACE'].value, namespace=current_app.config['SOCKET_NAMESPACE'],
messages=MESSAGES messages=MESSAGES
) )

View File

@ -160,19 +160,19 @@ def do_delete_database() -> Response:
def download_database() -> Response: def download_database() -> Response:
# Create a file name with a timestamp embedded # Create a file name with a timestamp embedded
name, extension = os.path.splitext( name, extension = os.path.splitext(
os.path.basename(current_app.config['DATABASE_PATH'].value) os.path.basename(current_app.config['DATABASE_PATH'])
) )
# Info # Info
logger.info('The database has been downloaded') logger.info('The database has been downloaded')
return send_file( return send_file(
current_app.config['DATABASE_PATH'].value, current_app.config['DATABASE_PATH'],
as_attachment=True, as_attachment=True,
download_name='{name}-{timestamp}{extension}'.format( download_name='{name}-{timestamp}{extension}'.format(
name=name, name=name,
timestamp=datetime.now().astimezone(g.timezone).strftime( timestamp=datetime.now().astimezone(g.timezone).strftime(
current_app.config['DATABASE_TIMESTAMP_FORMAT'].value current_app.config['DATABASE_TIMESTAMP_FORMAT']
), ),
extension=extension extension=extension
) )

View File

@ -114,7 +114,7 @@ def do_upload() -> Response:
file = upload_helper( file = upload_helper(
'file', 'file',
'instructions.upload', 'instructions.upload',
extensions=current_app.config['INSTRUCTIONS_ALLOWED_EXTENSIONS'].value, extensions=current_app.config['INSTRUCTIONS_ALLOWED_EXTENSIONS'],
) )
if isinstance(file, Response): if isinstance(file, Response):

View File

@ -18,7 +18,7 @@ class BrickWishList(BrickRecordList[BrickWish]):
def all(self, /) -> Self: def all(self, /) -> Self:
# Load the wished sets from the database # Load the wished sets from the database
for record in self.select( for record in self.select(
order=current_app.config['WISHES_DEFAULT_ORDER'].value order=current_app.config['WISHES_DEFAULT_ORDER']
): ):
brickwish = BrickWish(record=record) brickwish = BrickWish(record=record)

View File

@ -4,7 +4,7 @@
{% block main %} {% block main %}
<div class="container"> <div class="container">
{% if not config['HIDE_ADD_BULK_SET'].value %} {% if not config['HIDE_ADD_BULK_SET'] %}
<div class="alert alert-primary" role="alert"> <div class="alert alert-primary" role="alert">
<h4 class="alert-heading">Too many to add?</h4> <h4 class="alert-heading">Too many to add?</h4>
<p class="mb-0">You can import multiple sets at once with <a href="{{ url_for('add.bulk') }}" class="btn btn-primary"><i class="ri-function-add-line"></i> Bulk add</a>.</p> <p class="mb-0">You can import multiple sets at once with <a href="{{ url_for('add.bulk') }}" class="btn btn-primary"><i class="ri-function-add-line"></i> Bulk add</a>.</p>

View File

@ -21,7 +21,7 @@
{% else %} {% else %}
{% include 'admin/logout.html' %} {% include 'admin/logout.html' %}
{% include 'admin/instructions.html' %} {% include 'admin/instructions.html' %}
{% if not config['USE_REMOTE_IMAGES'].value %} {% if not config['USE_REMOTE_IMAGES'] %}
{% include 'admin/image.html' %} {% include 'admin/image.html' %}
{% endif %} {% endif %}
{% include 'admin/theme.html' %} {% include 'admin/theme.html' %}

View File

@ -6,7 +6,7 @@
{% if error %}<div class="alert alert-danger" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %} {% if error %}<div class="alert alert-danger" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %}
{% if not is_init %} {% if not is_init %}
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">
<p>The database file is: <code>{{ config['DATABASE_PATH'].value }}</code>. The database is not initialized.</p> <p>The database file is: <code>{{ config['DATABASE_PATH'] }}</code>. The database is not initialized.</p>
<hr> <hr>
<form action="{{ url_for('admin.init_database') }}" method="post" class="text-end"> <form action="{{ url_for('admin.init_database') }}" method="post" class="text-end">
<button type="submit" class="btn btn-warning"><i class="ri-reset-right-fill"></i> Initialize the database</button> <button type="submit" class="btn btn-warning"><i class="ri-reset-right-fill"></i> Initialize the database</button>
@ -25,7 +25,7 @@
</form> </form>
</div> </div>
{% endif %} {% endif %}
<p>The database file is: <code>{{ config['DATABASE_PATH'].value }}</code>. <i class="ri-checkbox-circle-line"></i> The database is initialized.</p> <p>The database file is: <code>{{ config['DATABASE_PATH'] }}</code>. <i class="ri-checkbox-circle-line"></i> The database is initialized.</p>
<p> <p>
<a href="{{ url_for('admin.download_database') }}" class="btn btn-primary" role="button"><i class="ri-download-line"></i> Download the database file</a> <a href="{{ url_for('admin.download_database') }}" class="btn btn-primary" role="button"><i class="ri-download-line"></i> Download the database file</a>
</p> </p>

View File

@ -3,8 +3,8 @@
{{ accordion.header('Instructions', 'instructions', 'admin', expanded=open_instructions, icon='file-line') }} {{ accordion.header('Instructions', 'instructions', 'admin', expanded=open_instructions, icon='file-line') }}
<h5 class="border-bottom">Folder</h5> <h5 class="border-bottom">Folder</h5>
<p> <p>
The instructions files folder is: <code>{{ config['INSTRUCTIONS_FOLDER'].value }}</code>. <br> The instructions files folder is: <code>{{ config['INSTRUCTIONS_FOLDER'] }}</code>. <br>
Allowed file formats for instructions are the following: <code>{{ ', '.join(config['INSTRUCTIONS_ALLOWED_EXTENSIONS'].value) }}</code>. Allowed file formats for instructions are the following: <code>{{ ', '.join(config['INSTRUCTIONS_ALLOWED_EXTENSIONS']) }}</code>.
</p> </p>
<h5 class="border-bottom">Counters</h5> <h5 class="border-bottom">Counters</h5>
<p> <p>

View File

@ -4,7 +4,7 @@
<h5 class="border-bottom">File</h5> <h5 class="border-bottom">File</h5>
{% if retired.exception %}<div class="alert alert-danger" role="alert">An exception occured while loading processing the retired sets: {{ retired.exception }}</div>{% endif %} {% if retired.exception %}<div class="alert alert-danger" role="alert">An exception occured while loading processing the retired sets: {{ retired.exception }}</div>{% endif %}
<p> <p>
The retired sets file is: <code>{{ config['RETIRED_SETS_PATH'].value }}</code>. The retired sets file is: <code>{{ config['RETIRED_SETS_PATH'] }}</code>.
{% if retired.size %}<span class="badge rounded-pill text-bg-info fw-normal"><i class="ri-hard-drive-line"></i> {{ retired.human_size() }}</span>{% endif %} {% if retired.size %}<span class="badge rounded-pill text-bg-info fw-normal"><i class="ri-hard-drive-line"></i> {{ retired.human_size() }}</span>{% endif %}
{% if retired.mtime %}<span class="badge rounded-pill text-bg-light border fw-normal"><i class="ri-calendar-line"></i> {{ retired.human_time() }}</span>{% endif %} {% if retired.mtime %}<span class="badge rounded-pill text-bg-light border fw-normal"><i class="ri-calendar-line"></i> {{ retired.human_time() }}</span>{% endif %}
</p> </p>

View File

@ -4,7 +4,7 @@
<h5 class="border-bottom">File</h5> <h5 class="border-bottom">File</h5>
{% if theme.exception %}<div class="alert alert-danger" role="alert">An exception occured while loading processing the themes: {{ theme.exception }}</div>{% endif %} {% if theme.exception %}<div class="alert alert-danger" role="alert">An exception occured while loading processing the themes: {{ theme.exception }}</div>{% endif %}
<p> <p>
The themes file is: <code>{{ config['THEMES_PATH'].value }}</code>. The themes file is: <code>{{ config['THEMES_PATH'] }}</code>.
{% if theme.size %}<span class="badge rounded-pill text-bg-info fw-normal"><i class="ri-hard-drive-line"></i> {{ theme.human_size() }}</span>{% endif %} {% if theme.size %}<span class="badge rounded-pill text-bg-info fw-normal"><i class="ri-hard-drive-line"></i> {{ theme.human_size() }}</span>{% endif %}
{% if theme.mtime %}<span class="badge rounded-pill text-bg-light border fw-normal"><i class="ri-calendar-line"></i> {{ theme.human_time() }}</span>{% endif %} {% if theme.mtime %}<span class="badge rounded-pill text-bg-light border fw-normal"><i class="ri-calendar-line"></i> {{ theme.human_time() }}</span>{% endif %}
</p> </p>

View File

@ -25,7 +25,7 @@
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav me-auto mb-2 mb-lg-0">
{% for item in config['_NAVBAR'] %} {% for item in config['_NAVBAR'] %}
{% if item.flag and not config[item.flag].value %} {% if item.flag and not config[item.flag] %}
<li class="nav-item px-1"> <li class="nav-item px-1">
<a {% if request.url_rule.endpoint == item.endpoint %}class="nav-link active" aria-current="page"{% else %}class="nav-link"{% endif %} href="{{ url_for(item.endpoint) }}"> <a {% if request.url_rule.endpoint == item.endpoint %}class="nav-link active" aria-current="page"{% else %}class="nav-link"{% endif %} href="{{ url_for(item.endpoint) }}">
{% if item.icon %} {% if item.icon %}
@ -61,7 +61,7 @@
<div class="col-md-6 d-flex justify-content-center"> <div class="col-md-6 d-flex justify-content-center">
<small> <small>
<i class="ri-timer-2-line"></i> {{ g.request_time() }} <i class="ri-timer-2-line"></i> {{ g.request_time() }}
{%if config['DEBUG'].value and g.database_stats %} {%if config['DEBUG'] and g.database_stats %}
| <i class="ri-database-2-line"></i> {{g.database_stats.print() }} | <i class="ri-database-2-line"></i> {{g.database_stats.print() }}
{% endif %} {% endif %}
</small> </small>

View File

@ -3,8 +3,8 @@
{% block main %} {% block main %}
<div class="container-fluid"> <div class="container-fluid">
<h2 class="border-bottom lh-base pb-1"> <h2 class="border-bottom lh-base pb-1">
<i class="ri-hashtag"></i> {% if config['RANDOM'].value %}Random selection of{% else %}Latest added{% endif %} sets <i class="ri-hashtag"></i> {% if config['RANDOM'] %}Random selection of{% else %}Latest added{% endif %} sets
{% if not config['HIDE_ALL_SETS'].value %} {% if not config['HIDE_ALL_SETS'] %}
<a href="{{ url_for('set.list') }}" class="btn btn-sm btn-primary ms-1">All sets</a> <a href="{{ url_for('set.list') }}" class="btn btn-sm btn-primary ms-1">All sets</a>
{% endif %} {% endif %}
</h2> </h2>
@ -23,8 +23,8 @@
{% endif %} {% endif %}
{% if minifigure_collection | length %} {% if minifigure_collection | length %}
<h2 class="border-bottom lh-base pb-1"> <h2 class="border-bottom lh-base pb-1">
<i class="ri-group-line"></i> {% if config['RANDOM'].value %}Random selection of{% else %}Latest added{% endif %} minifigures <i class="ri-group-line"></i> {% if config['RANDOM'] %}Random selection of{% else %}Latest added{% endif %} minifigures
{% if not config['HIDE_ALL_MINIFIGURES'].value %} {% if not config['HIDE_ALL_MINIFIGURES'] %}
<a href="{{ url_for('minifigure.list') }}" class="btn btn-sm btn-primary ms-1">All minifigures</a> <a href="{{ url_for('minifigure.list') }}" class="btn btn-sm btn-primary ms-1">All minifigures</a>
{% endif %} {% endif %}
</h2> </h2>

View File

@ -26,8 +26,8 @@
<div class="mb-3"> <div class="mb-3">
<label for="file" class="form-label">Instructions file</label> <label for="file" class="form-label">Instructions file</label>
<div class="input-group"> <div class="input-group">
<input type="file" class="form-control" id="file" name="file" accept="{{ ','.join(config['INSTRUCTIONS_ALLOWED_EXTENSIONS'].value) }}"> <input type="file" class="form-control" id="file" name="file" accept="{{ ','.join(config['INSTRUCTIONS_ALLOWED_EXTENSIONS']) }}">
<span class="input-group-text">{{ ', '.join(config['INSTRUCTIONS_ALLOWED_EXTENSIONS'].value) }}</span> <span class="input-group-text">{{ ', '.join(config['INSTRUCTIONS_ALLOWED_EXTENSIONS']) }}</span>
</div> </div>
</div> </div>
<div class="text-end"> <div class="text-end">

View File

@ -16,7 +16,7 @@
{% endif %} {% endif %}
</button> </button>
</h2> </h2>
<div id="{{ id }}" class="accordion-collapse collapse {% if expanded %}show{% endif %}" {% if not config['INDEPENDENT_ACCORDIONS'].value %}data-bs-parent="#{{ parent }}"{% endif %}> <div id="{{ id }}" class="accordion-collapse collapse {% if expanded %}show{% endif %}" {% if not config['INDEPENDENT_ACCORDIONS'] %}data-bs-parent="#{{ parent }}"{% endif %}>
<div class="accordion-body {% if class %}{{ class }}{% endif %}"> <div class="accordion-body {% if class %}{{ class }}{% endif %}">
{% endmacro %} {% endmacro %}

View File

@ -60,7 +60,7 @@
{% if number %}{ select: [{{ number }}], type: "number", searchable: false },{% endif %} {% if number %}{ select: [{{ number }}], type: "number", searchable: false },{% endif %}
], ],
pagerDelta: 1, pagerDelta: 1,
perPage: {{ config['DEFAULT_TABLE_PER_PAGE'].value }}, perPage: {{ config['DEFAULT_TABLE_PER_PAGE'] }},
perPageSelect: [10, 25, 50, 100, 500, 1000], perPageSelect: [10, 25, 50, 100, 500, 1000],
searchable: true, searchable: true,
searchQuerySeparator: "", searchQuerySeparator: "",