From 632856cf7691072fca72fc7f300f6aa91cbea82e Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sat, 29 Nov 2025 10:37:29 +0000 Subject: [PATCH 1/7] fix(client): Load pages from backend when navigating backwards beyond buffer (#693) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed "Prev page" button not loading page content from backend when navigating beyond the pre-loaded buffer in script and cue config pages. Changes: - ScriptEditor: Load target page from backend before copying to TMP_SCRIPT buffer - CueEditor: Load target page from backend before decrementing current page 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- client/src/vue_components/show/config/cues/CueEditor.vue | 7 ++++++- .../src/vue_components/show/config/script/ScriptEditor.vue | 7 +++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/client/src/vue_components/show/config/cues/CueEditor.vue b/client/src/vue_components/show/config/cues/CueEditor.vue index 3aaf2cab..4bb7cab2 100644 --- a/client/src/vue_components/show/config/cues/CueEditor.vue +++ b/client/src/vue_components/show/config/cues/CueEditor.vue @@ -254,9 +254,14 @@ export default { DATA: {}, }); }, - decrPage() { + async decrPage() { if (this.currentEditPage > 1) { + const targetPage = this.currentEditPage - 1; + // Load target page from backend + await this.LOAD_SCRIPT_PAGE(targetPage); this.currentEditPage--; + // Pre-load previous page + await this.LOAD_SCRIPT_PAGE(this.currentEditPage - 1); } }, async incrPage() { diff --git a/client/src/vue_components/show/config/script/ScriptEditor.vue b/client/src/vue_components/show/config/script/ScriptEditor.vue index 4ee4b980..29d11371 100644 --- a/client/src/vue_components/show/config/script/ScriptEditor.vue +++ b/client/src/vue_components/show/config/script/ScriptEditor.vue @@ -533,8 +533,11 @@ export default { }, async decrPage() { if (this.currentEditPage > 1) { - if (!Object.keys(this.TMP_SCRIPT).includes((this.currentEditPage - 1).toString())) { - this.ADD_BLANK_PAGE(this.currentEditPage - 1); + const targetPage = this.currentEditPage - 1; + // Load from backend if not in buffer + if (!Object.keys(this.TMP_SCRIPT).includes(targetPage.toString())) { + await this.LOAD_SCRIPT_PAGE(targetPage); + this.ADD_BLANK_PAGE(targetPage); } if (this.TMP_SCRIPT[this.currentEditPageKey].length === 0) { this.REMOVE_PAGE(this.currentEditPage); From 00f56e96edf0995770e215411bb497d8335219be Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sat, 29 Nov 2025 19:35:09 +0000 Subject: [PATCH 2/7] feat(test): Upgrade pytest to version 9.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgraded pytest from <8.5 to <9.1 to adopt the latest testing framework features and improvements. Changes: - Updated pytest requirement from <8.5 to <9.1 - Updated pytest-asyncio to >=1.3.0 for pytest 9 compatibility Testing: - All 31 tests pass successfully - No pytest-specific deprecation warnings detected - Verified compatibility with existing test suite including: - Tornado AsyncHTTPTestCase tests - pytest-asyncio async tests - pytest fixtures The upgrade is safe and backward-compatible with our current test patterns. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- server/test_requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/test_requirements.txt b/server/test_requirements.txt index 33bd85b8..0e0091d9 100644 --- a/server/test_requirements.txt +++ b/server/test_requirements.txt @@ -1,5 +1,5 @@ -pytest<8.5 -pytest-asyncio +pytest<9.1 +pytest-asyncio>=1.3.0 pylint==3.3.9 black==25.11.0 isort==5.13.2 \ No newline at end of file From 48765a84afaefd9ab47ec4c54656cc7306121bb2 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Mon, 1 Dec 2025 19:35:39 +0000 Subject: [PATCH 3/7] feat(tooling): Migrate from black, isort, and pylint to ruff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace three separate linting/formatting tools with ruff, a fast all-in-one Python linter and formatter that provides equivalent functionality. Changes: - Replaced black, isort, and pylint with ruff in test_requirements.txt - Created comprehensive ruff configuration in pyproject.toml that matches the exact behavior of the previous tools - Updated GitHub Actions workflow (pylint.yml) to use ruff instead of three separate jobs - Applied ruff formatting and import sorting to normalize codebase Configuration equivalence: - Formatting: Matches black's 88-character line length and formatting rules - Import sorting: Matches isort's black-compatible profile with same known-first-party packages - Linting: Matches pylint's disabled rules and design limits (max-args, max-locals, max-branches, etc.) Testing: - All 31 tests pass - Ruff check passes with no errors - Ruff format produces no changes (codebase is normalized) Benefits: - Single tool instead of three (simpler dependency management) - Faster execution (ruff is written in Rust) - Consistent configuration in one place (pyproject.toml) - Reduced GitHub Actions runtime (one job instead of three) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/pylint.yml | 55 +------ server/controllers/api/auth.py | 1 - server/controllers/api/rbac.py | 3 - server/controllers/api/show/acts.py | 2 - server/controllers/api/show/cast.py | 1 - server/controllers/api/show/characters.py | 2 - server/controllers/api/show/cues.py | 2 - server/controllers/api/show/microphones.py | 2 - server/controllers/api/show/scenes.py | 1 - .../controllers/api/show/script/revisions.py | 2 - server/controllers/api/show/script/script.py | 88 +++++------ server/controllers/api/show/sessions.py | 1 - server/controllers/api/show/shows.py | 2 - server/controllers/api/websocket.py | 1 - server/controllers/controllers.py | 1 + server/controllers/ws_controller.py | 8 +- server/digi_server/app_server.py | 5 +- server/digi_server/logger.py | 1 + server/digi_server/settings.py | 6 +- server/main.py | 2 + server/models/models.py | 1 + server/models/script.py | 1 + server/pyproject.toml | 148 +++++++++++------- server/rbac/rbac.py | 2 +- server/rbac/rbac_db.py | 6 +- server/registry/schema.py | 1 + server/schemas/schemas.py | 3 +- server/test/test_auth_api.py | 1 - server/test/test_settings.py | 4 +- server/test/test_utils.py | 1 - server/test_requirements.txt | 4 +- server/utils/database.py | 2 - server/utils/file_watcher.py | 4 +- server/utils/module_discovery.py | 1 + server/utils/web/base_controller.py | 7 +- server/utils/web/route.py | 2 +- 36 files changed, 164 insertions(+), 210 deletions(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 2ce8f47f..f9059835 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -3,8 +3,8 @@ name: Python Linting and Formatting on: [push] jobs: - pylint: - name: Pylint + ruff: + name: Ruff (Linting & Formatting) runs-on: ubuntu-latest defaults: run: @@ -22,52 +22,9 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt -r test_requirements.txt - - name: Analysing the code with pylint + - name: Run ruff linter run: | - pylint $(git ls-files '*.py') --output-format github - - black: - name: Black - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./server - strategy: - matrix: - python-version: ["3.13"] - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt -r test_requirements.txt - - name: Check code formatting with black - run: | - black --check $(git ls-files '*.py') - - isort: - name: isort - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./server - strategy: - matrix: - python-version: ["3.13"] - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt -r test_requirements.txt - - name: Check import sorting with isort + ruff check $(git ls-files '*.py') --output-format github + - name: Run ruff formatter run: | - isort --check $(git ls-files '*.py') --profile=black \ No newline at end of file + ruff format --check $(git ls-files '*.py') diff --git a/server/controllers/api/auth.py b/server/controllers/api/auth.py index 32ca5ab4..0c4ebcfe 100644 --- a/server/controllers/api/auth.py +++ b/server/controllers/api/auth.py @@ -21,7 +21,6 @@ @ApiRoute("auth/create", ApiVersion.V1) class UserCreateController(BaseAPIController): - async def post(self): data = escape.json_decode(self.request.body) diff --git a/server/controllers/api/rbac.py b/server/controllers/api/rbac.py index c8bcaf4c..e93b58ba 100644 --- a/server/controllers/api/rbac.py +++ b/server/controllers/api/rbac.py @@ -94,7 +94,6 @@ async def get(self): @ApiRoute("rbac/user/roles", ApiVersion.V1) class RBACUserRolesHandler(BaseAPIController): - @api_authenticated async def get(self): with self.make_session() as session: @@ -117,7 +116,6 @@ async def get(self): @ApiRoute("rbac/user/roles/grant", ApiVersion.V1) class RBACRolesGrantHandler(BaseAPIController): - @api_authenticated @require_admin async def post(self): @@ -175,7 +173,6 @@ async def post(self): @ApiRoute("rbac/user/roles/revoke", ApiVersion.V1) class RBACRolesRevokeHandler(BaseAPIController): - @api_authenticated @require_admin async def post(self): diff --git a/server/controllers/api/show/acts.py b/server/controllers/api/show/acts.py index 21ef879e..4621e0f7 100644 --- a/server/controllers/api/show/acts.py +++ b/server/controllers/api/show/acts.py @@ -12,7 +12,6 @@ @ApiRoute("show/act", ApiVersion.V1) class ActController(BaseAPIController): - @requires_show def get(self): current_show = self.get_current_show() @@ -212,7 +211,6 @@ async def delete(self): @ApiRoute("show/act/first_scene", ApiVersion.V1) class FirstSceneController(BaseAPIController): - @requires_show @no_live_session async def post(self): diff --git a/server/controllers/api/show/cast.py b/server/controllers/api/show/cast.py index d86cd752..d16d53e3 100644 --- a/server/controllers/api/show/cast.py +++ b/server/controllers/api/show/cast.py @@ -13,7 +13,6 @@ @ApiRoute("show/cast", ApiVersion.V1) class CastController(BaseAPIController): - @requires_show def get(self): current_show = self.get_current_show() diff --git a/server/controllers/api/show/characters.py b/server/controllers/api/show/characters.py index f249be93..397231af 100644 --- a/server/controllers/api/show/characters.py +++ b/server/controllers/api/show/characters.py @@ -13,7 +13,6 @@ @ApiRoute("show/character", ApiVersion.V1) class CharacterController(BaseAPIController): - @requires_show def get(self): current_show = self.get_current_show() @@ -222,7 +221,6 @@ async def get(self): @ApiRoute("show/character/group", ApiVersion.V1) class CharacterGroupController(BaseAPIController): - @requires_show def get(self): current_show = self.get_current_show() diff --git a/server/controllers/api/show/cues.py b/server/controllers/api/show/cues.py index 26c01f2f..d556aaba 100644 --- a/server/controllers/api/show/cues.py +++ b/server/controllers/api/show/cues.py @@ -15,7 +15,6 @@ @ApiRoute("show/cues/types", ApiVersion.V1) class CueTypesController(BaseAPIController): - @requires_show def get(self): current_show = self.get_current_show() @@ -165,7 +164,6 @@ async def delete(self): @ApiRoute("show/cues", ApiVersion.V1) class CueController(BaseAPIController): - @requires_show def get(self): current_show = self.get_current_show() diff --git a/server/controllers/api/show/microphones.py b/server/controllers/api/show/microphones.py index cc7997e4..0c7ef5cf 100644 --- a/server/controllers/api/show/microphones.py +++ b/server/controllers/api/show/microphones.py @@ -13,7 +13,6 @@ @ApiRoute("show/microphones", ApiVersion.V1) class MicrophoneController(BaseAPIController): - @requires_show def get(self): current_show = self.get_current_show() @@ -181,7 +180,6 @@ async def delete(self): @ApiRoute("show/microphones/allocations", ApiVersion.V1) class MicrophoneAllocationsController(BaseAPIController): - @requires_show def get(self): current_show = self.get_current_show() diff --git a/server/controllers/api/show/scenes.py b/server/controllers/api/show/scenes.py index fb20efb7..0d0945c0 100644 --- a/server/controllers/api/show/scenes.py +++ b/server/controllers/api/show/scenes.py @@ -12,7 +12,6 @@ @ApiRoute("show/scene", ApiVersion.V1) class SceneController(BaseAPIController): - @requires_show def get(self): current_show = self.get_current_show() diff --git a/server/controllers/api/show/script/revisions.py b/server/controllers/api/show/script/revisions.py index af60500b..a55d694b 100644 --- a/server/controllers/api/show/script/revisions.py +++ b/server/controllers/api/show/script/revisions.py @@ -23,7 +23,6 @@ @ApiRoute("show/script/revisions", ApiVersion.V1) class ScriptRevisionsController(BaseAPIController): - @requires_show def get(self): current_show = self.get_current_show() @@ -244,7 +243,6 @@ async def delete(self): @ApiRoute("show/script/revisions/current", ApiVersion.V1) class ScriptCurrentRevisionController(BaseAPIController): - @requires_show def get(self): current_show = self.get_current_show() diff --git a/server/controllers/api/show/script/script.py b/server/controllers/api/show/script/script.py index 748b2cc7..6c15d96a 100644 --- a/server/controllers/api/show/script/script.py +++ b/server/controllers/api/show/script/script.py @@ -25,7 +25,6 @@ @ApiRoute("/show/script", ApiVersion.V1) class ScriptController(BaseAPIController): - @requires_show def get(self): current_show = self.get_current_show() @@ -177,7 +176,6 @@ async def post(self): previous_line: Optional[ScriptLineRevisionAssociation] = None for index, line in enumerate(lines): - # Validate each line before we do anything with it valid_status, valid_reason = self._validate_line(line) if not valid_status: @@ -508,13 +506,13 @@ async def patch(self): session.flush() if previous_line.next_line: - next_association: ( - ScriptLineRevisionAssociation - ) = session.query(ScriptLineRevisionAssociation).get( - { - "revision_id": revision.id, - "line_id": previous_line.next_line.id, - } + next_association: ScriptLineRevisionAssociation = ( + session.query(ScriptLineRevisionAssociation).get( + { + "revision_id": revision.id, + "line_id": previous_line.next_line.id, + } + ) ) next_association.previous_line = line_object line_association.next_line = next_association.line @@ -535,50 +533,50 @@ async def patch(self): and curr_association.previous_line ): # Next line and previous line, so need to update both - next_association: ( - ScriptLineRevisionAssociation - ) = session.query(ScriptLineRevisionAssociation).get( - { - "revision_id": revision.id, - "line_id": curr_association.next_line.id, - } + next_association: ScriptLineRevisionAssociation = ( + session.query(ScriptLineRevisionAssociation).get( + { + "revision_id": revision.id, + "line_id": curr_association.next_line.id, + } + ) ) next_association.previous_line = ( curr_association.previous_line ) session.flush() - prev_association: ( - ScriptLineRevisionAssociation - ) = session.query(ScriptLineRevisionAssociation).get( - { - "revision_id": revision.id, - "line_id": curr_association.previous_line.id, - } + prev_association: ScriptLineRevisionAssociation = ( + session.query(ScriptLineRevisionAssociation).get( + { + "revision_id": revision.id, + "line_id": curr_association.previous_line.id, + } + ) ) prev_association.next_line = next_association.line session.flush() elif curr_association.next_line: # No previous line, so need to update next line only - next_association: ( - ScriptLineRevisionAssociation - ) = session.query(ScriptLineRevisionAssociation).get( - { - "revision_id": revision.id, - "line_id": curr_association.next_line.id, - } + next_association: ScriptLineRevisionAssociation = ( + session.query(ScriptLineRevisionAssociation).get( + { + "revision_id": revision.id, + "line_id": curr_association.next_line.id, + } + ) ) next_association.previous_line = None session.flush() elif curr_association.previous_line: # No next line, so need to update previous line only - prev_association: ( - ScriptLineRevisionAssociation - ) = session.query(ScriptLineRevisionAssociation).get( - { - "revision_id": revision.id, - "line_id": curr_association.previous_line.id, - } + prev_association: ScriptLineRevisionAssociation = ( + session.query(ScriptLineRevisionAssociation).get( + { + "revision_id": revision.id, + "line_id": curr_association.previous_line.id, + } + ) ) prev_association.next_line = None session.flush() @@ -616,13 +614,13 @@ async def patch(self): curr_association.previous_line = previous_line.line if curr_association.next_line: - next_association: ( - ScriptLineRevisionAssociation - ) = session.query(ScriptLineRevisionAssociation).get( - { - "revision_id": revision.id, - "line_id": curr_association.next_line.id, - } + next_association: ScriptLineRevisionAssociation = ( + session.query(ScriptLineRevisionAssociation).get( + { + "revision_id": revision.id, + "line_id": curr_association.next_line.id, + } + ) ) next_association.previous_line = line_object session.flush() @@ -687,7 +685,6 @@ def get(self): @ApiRoute("/show/script/cuts", ApiVersion.V1) class ScriptCutsController(BaseAPIController): - @requires_show def get(self): current_show = self.get_current_show() @@ -787,7 +784,6 @@ def put(self): @ApiRoute("/show/script/max_page", ApiVersion.V1) class ScriptMaxPageController(BaseAPIController): - @requires_show def get(self): current_show = self.get_current_show() diff --git a/server/controllers/api/show/sessions.py b/server/controllers/api/show/sessions.py index 804b509e..0ed81ab9 100644 --- a/server/controllers/api/show/sessions.py +++ b/server/controllers/api/show/sessions.py @@ -13,7 +13,6 @@ @ApiRoute("show/sessions", ApiVersion.V1) class SessionsController(BaseAPIController): - @requires_show def get(self): current_show = self.get_current_show() diff --git a/server/controllers/api/show/shows.py b/server/controllers/api/show/shows.py index c6369d0d..b96f24a2 100644 --- a/server/controllers/api/show/shows.py +++ b/server/controllers/api/show/shows.py @@ -15,7 +15,6 @@ @ApiRoute("show", ApiVersion.V1) class ShowController(BaseAPIController): - @api_authenticated @require_admin async def post(self): @@ -207,7 +206,6 @@ async def patch(self): @ApiRoute("shows", ApiVersion.V1) class ShowsController(BaseAPIController): - def get(self): shows = [] show_schema = ShowSchema() diff --git a/server/controllers/api/websocket.py b/server/controllers/api/websocket.py index 0bb54cda..5a0f304a 100644 --- a/server/controllers/api/websocket.py +++ b/server/controllers/api/websocket.py @@ -6,7 +6,6 @@ @ApiRoute("ws/sessions", ApiVersion.V1) class WebsocketSessionsController(BaseAPIController): - def get(self): session_scheme = SessionSchema() with self.make_session() as session: diff --git a/server/controllers/controllers.py b/server/controllers/controllers.py index b913cbcb..cb29d99f 100644 --- a/server/controllers/controllers.py +++ b/server/controllers/controllers.py @@ -9,6 +9,7 @@ from utils.web.base_controller import BaseAPIController, BaseController from utils.web.route import ApiRoute, ApiVersion, Route + IMPORTED_CONTROLLERS = {} diff --git a/server/controllers/ws_controller.py b/server/controllers/ws_controller.py index 4279f1bf..19b5cc28 100644 --- a/server/controllers/ws_controller.py +++ b/server/controllers/ws_controller.py @@ -15,13 +15,13 @@ from models.user import User from utils.web.route import ApiRoute, ApiVersion + if TYPE_CHECKING: from digi_server.app_server import DigiScriptServer @ApiRoute("ws", ApiVersion.V1) class WebSocketController(SessionMixin, WebSocketHandler): - def __init__(self, application, request, **kwargs): super().__init__(application, request, **kwargs) # pylint: disable=used-before-assignment @@ -195,9 +195,7 @@ async def authenticate_with_token(self, token): ) return True - async def on_message( - self, message: Union[str, bytes] - ): # pylint: disable=invalid-overridden-method + async def on_message(self, message: Union[str, bytes]): # pylint: disable=invalid-overridden-method get_logger().debug( f"WebSocket received data from {self.request.remote_ip}: {message}" ) @@ -424,7 +422,7 @@ def write_message( except WebSocketClosedError: get_logger().error( f"Trying to send message to closed websocket " - f'{self.__getattribute__("internal_id")} at IP address ' + f"{self.__getattribute__('internal_id')} at IP address " f"{self.request.remote_ip}, closing." ) self.on_close() diff --git a/server/digi_server/app_server.py b/server/digi_server/app_server.py index e740454b..a56e1936 100644 --- a/server/digi_server/app_server.py +++ b/server/digi_server/app_server.py @@ -33,10 +33,7 @@ from utils.web.route import Route -class DigiScriptServer( - PrometheusMixIn, Application -): # pylint: disable=too-many-instance-attributes - +class DigiScriptServer(PrometheusMixIn, Application): # pylint: disable=too-many-instance-attributes def __init__( self, debug=False, diff --git a/server/digi_server/logger.py b/server/digi_server/logger.py index 2280dc6d..c7fc4f10 100644 --- a/server/digi_server/logger.py +++ b/server/digi_server/logger.py @@ -3,6 +3,7 @@ from tornado.log import LogFormatter + logger = logging.getLogger("DigiScript") diff --git a/server/digi_server/settings.py b/server/digi_server/settings.py index bb17f444..f6f2a1f8 100644 --- a/server/digi_server/settings.py +++ b/server/digi_server/settings.py @@ -9,6 +9,7 @@ from digi_server.logger import get_logger from utils.file_watcher import IOLoopFileWatcher + if TYPE_CHECKING: from digi_server.app_server import DigiScriptServer @@ -109,7 +110,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None): self.settings = {} - db_default = f'sqlite:///{os.path.join(os.path.dirname(__file__), "../conf/digiscript.sqlite")}' + db_default = f"sqlite:///{os.path.join(os.path.dirname(__file__), '../conf/digiscript.sqlite')}" self.define( "has_admin_user", bool, @@ -310,8 +311,7 @@ async def set(self, key, item): async with self.lock: if key not in self.settings: get_logger().warning( - f"Setting {key} found in settings file is not " - f"defined, ignoring!" + f"Setting {key} found in settings file is not defined, ignoring!" ) else: changed = self.settings[key].set_value(item) diff --git a/server/main.py b/server/main.py index 772a2da1..2ae1f4ce 100755 --- a/server/main.py +++ b/server/main.py @@ -5,6 +5,7 @@ from tornado.options import define, options, parse_command_line + # Add PyInstaller support try: from utils.pyinstaller_utils import ( @@ -33,6 +34,7 @@ def ensure_writable_db_path(path): from digi_server.app_server import DigiScriptServer from digi_server.logger import add_logging_level, get_logger + add_logging_level("TRACE", logging.DEBUG - 5) get_logger().setLevel(logging.DEBUG) diff --git a/server/models/models.py b/server/models/models.py index 25e48993..b6e65fad 100644 --- a/server/models/models.py +++ b/server/models/models.py @@ -2,6 +2,7 @@ from utils.database import DigiSQLAlchemy from utils.module_discovery import import_modules + IMPORTED_MODELS = {} diff --git a/server/models/script.py b/server/models/script.py index 0a73aac4..6597390d 100644 --- a/server/models/script.py +++ b/server/models/script.py @@ -16,6 +16,7 @@ from registry.user_overrides import UserOverridesRegistry from utils.database import DeleteMixin + if TYPE_CHECKING: from digi_server.app_server import DigiScriptServer diff --git a/server/pyproject.toml b/server/pyproject.toml index 0163046c..6db47620 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -53,75 +53,107 @@ markers = [ ] # ============================================================================ -# BLACK CONFIGURATION +# RUFF CONFIGURATION # ============================================================================ -[tool.black] -# Use Black's default line-length of 88 to match existing codebase formatting +# Ruff replaces black, isort, and pylint with a single, fast tool +# https://docs.astral.sh/ruff/ + +[tool.ruff] +# Same line length as black (88) line-length = 88 -target-version = ['py313'] -include = '\.pyi?$' -extend-exclude = ''' -/( - # Exclude alembic migrations - | alembic_config/versions -)/ -''' +target-version = "py313" -# ============================================================================ -# ISORT CONFIGURATION -# ============================================================================ -[tool.isort] -profile = "black" -# Match Black's line length of 88 -line_length = 88 -skip_gitignore = true -extend_skip = ["alembic_config/versions"] -known_first_party = ["digi_server", "models", "controllers", "utils", "schemas", "rbac", "registry"] +# Exclude alembic migrations and other generated files +extend-exclude = [ + "alembic_config/versions", +] -# ============================================================================ -# PYLINT CONFIGURATION -# ============================================================================ -[tool.pylint.main] -# Add current directory to path for imports -init-hook = "from pylint.config import find_default_config_files; import sys; sys.path.append(next(find_default_config_files()).parent.as_posix())" - -# Ignore patterns -ignore-paths = [ - "^alembic_config/.*$", - "^test/.*$", +[tool.ruff.format] +# Use black-compatible formatting +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" +# Docstring formatting +docstring-code-format = false +docstring-code-line-length = "dynamic" + +[tool.ruff.lint] +# Enable pycodestyle (E, W), Pyflakes (F), isort (I), and pylint-like rules (PL) +# Note: We only enable rules that match the original black/isort/pylint behavior +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # Pyflakes + "I", # isort + "PL", # Pylint ] -[tool.pylint."messages control"] -disable = [ - "logging-fstring-interpolation", - "missing-module-docstring", - "missing-class-docstring", - "missing-function-docstring", - "too-few-public-methods", - "duplicate-code", - "too-many-return-statements", - "too-many-branches", - "too-many-statements", - "unnecessary-lambda", - "too-many-locals", - "too-many-nested-blocks", - "too-many-arguments", - "unnecessary-dunder-call", - "broad-exception-raised", - "broad-exception-caught", - "fixme", +# Ignore rules that match pylint's disabled checks +ignore = [ + # Line length in comments (black doesn't enforce this) + "E501", # Line too long + + # Documentation (pylint: missing-*-docstring) + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D107", # Missing docstring in __init__ + + # Complexity rules (pylint: too-many-*) + "PLR0911", # Too many return statements + "PLR0912", # Too many branches + "PLR0913", # Too many arguments + "PLR0915", # Too many statements + "PLR0917", # Too many positional arguments + "PLR2004", # Magic value used in comparison + + # Design (pylint: too-few-public-methods) + "PLR6301", # Method could be a function + + # Other pylint equivalents + "PLE0604", # Invalid object in __all__ + "PLW2901", # Redefined loop variable + "PLR1714", # Consider merging isinstance calls + "SIM102", # Use single if instead of nested + "SIM108", # Use ternary operator + "SIM114", # Combine if branches + "SIM115", # Use context manager + "SIM117", # Use single with statement + + # Broad exceptions (pylint: broad-exception-*) + "BLE001", # Blind except Exception + "TRY002", # Create exception with error message + "TRY003", # Long exception messages + "EM101", # Exception string literal + "EM102", # Exception f-string + + # Fixme comments + "FIX002", # Line contains TODO + "TD002", # Missing TODO author + "TD003", # Missing TODO link ] -[tool.pylint.format] -# Note: Pylint's max-line-length is 120 but Black enforces 88 -# This is intentional - Pylint checks logic, Black formats code -max-line-length = 120 +[tool.ruff.lint.per-file-ignores] +# Ignore test directory (matching pylint's ignore-paths) +"test/**" = ["ALL"] +"alembic_config/**" = ["ALL"] + +[tool.ruff.lint.isort] +# Match isort configuration +known-first-party = ["digi_server", "models", "controllers", "utils", "schemas", "rbac", "registry"] +force-single-line = false +lines-after-imports = 2 -[tool.pylint.design] +[tool.ruff.lint.pylint] +# Match pylint design limits max-args = 15 max-locals = 20 max-returns = 20 max-branches = 20 max-statements = 50 -max-attributes = 20 -max-positional-arguments = 15 +max-public-methods = 20 +max-positional-args = 15 diff --git a/server/rbac/rbac.py b/server/rbac/rbac.py index c71e9fad..a3fa8b9c 100644 --- a/server/rbac/rbac.py +++ b/server/rbac/rbac.py @@ -6,12 +6,12 @@ from rbac.role import Role from registry.schema import get_registry + if TYPE_CHECKING: from digi_server.app_server import DigiScriptServer class RBACController: - def __init__(self, app: "DigiScriptServer"): self.app = app self._rbac_db = RBACDatabase(app.get_db(), app) diff --git a/server/rbac/rbac_db.py b/server/rbac/rbac_db.py index dd9b415f..55758a4f 100644 --- a/server/rbac/rbac_db.py +++ b/server/rbac/rbac_db.py @@ -15,6 +15,7 @@ from utils import tree from utils.database import DigiDBSession, DigiSQLAlchemy + if TYPE_CHECKING: from digi_server.app_server import DigiScriptServer @@ -76,7 +77,6 @@ def _get_mapping_columns( class RBACDatabase: - def __init__(self, _db: DigiSQLAlchemy, app: "DigiScriptServer"): self._db: DigiSQLAlchemy = _db self._app = app @@ -267,9 +267,7 @@ def delete_resource(self, resource: db.Model): resource_inspect.mapper.mapped_table.fullname, [] ) for actor in actor_mappings: - table_name = ( - f"rbac_{actor}_" f"{resource_inspect.mapper.mapped_table.fullname}" - ) + table_name = f"rbac_{actor}_{resource_inspect.mapper.mapped_table.fullname}" self._delete_from_rbac_db(table_name, resource_cols) @functools.lru_cache() diff --git a/server/registry/schema.py b/server/registry/schema.py index 2755075c..4e5f7f53 100644 --- a/server/registry/schema.py +++ b/server/registry/schema.py @@ -2,6 +2,7 @@ from marshmallow_sqlalchemy import SQLAlchemySchema + if TYPE_CHECKING: from models.models import db diff --git a/server/schemas/schemas.py b/server/schemas/schemas.py index aac3af25..3aa05e03 100644 --- a/server/schemas/schemas.py +++ b/server/schemas/schemas.py @@ -144,7 +144,8 @@ class Meta: include_fk = True line_parts = Nested( - lambda: ScriptLinePartSchema(), many=True # pylint:disable=unnecessary-lambda + lambda: ScriptLinePartSchema(), + many=True, # pylint:disable=unnecessary-lambda ) diff --git a/server/test/test_auth_api.py b/server/test/test_auth_api.py index d5e7cfcf..2686aad4 100644 --- a/server/test/test_auth_api.py +++ b/server/test/test_auth_api.py @@ -4,7 +4,6 @@ class TestAuthAPI(DigiScriptTestCase): - def test_get(self): response = self.fetch("/api/v1/auth/create") self.assertEqual(405, response.code) diff --git a/server/test/test_settings.py b/server/test/test_settings.py index a2f6441d..bac54a7a 100644 --- a/server/test/test_settings.py +++ b/server/test/test_settings.py @@ -6,14 +6,12 @@ class TestSettings(DigiScriptTestCase): - @gen_test def test_set_invalid_name(self): yield self._app.digi_settings.set("not_present_key", "some_value") self.assertLogs( get_logger(), - "Setting not_present_key found in settings file is not " - "defined, ignoring!", + "Setting not_present_key found in settings file is not defined, ignoring!", ) @gen_test diff --git a/server/test/test_utils.py b/server/test/test_utils.py index f2f5d942..7363ddb7 100644 --- a/server/test/test_utils.py +++ b/server/test/test_utils.py @@ -9,7 +9,6 @@ class DigiScriptTestCase(AsyncHTTPTestCase): - def get_app(self): return DigiScriptServer( debug=True, diff --git a/server/test_requirements.txt b/server/test_requirements.txt index 0e0091d9..459223eb 100644 --- a/server/test_requirements.txt +++ b/server/test_requirements.txt @@ -1,5 +1,3 @@ pytest<9.1 pytest-asyncio>=1.3.0 -pylint==3.3.9 -black==25.11.0 -isort==5.13.2 \ No newline at end of file +ruff==0.9.3 \ No newline at end of file diff --git a/server/utils/database.py b/server/utils/database.py index bbe91633..86448d40 100644 --- a/server/utils/database.py +++ b/server/utils/database.py @@ -15,7 +15,6 @@ def post_delete(self, session: "DigiDBSession"): class DigiDBSession(SessionEx): - def _delete_impl(self, state, obj, head): for hook in self.db.delete_hooks: hook(self, obj) @@ -28,7 +27,6 @@ def _delete_impl(self, state, obj, head): class DigiSQLAlchemy(SQLAlchemy): - def __init__(self, url=None, binds=None, session_options=None, engine_options=None): self.sessionmaker = None # Store the original create_engine method diff --git a/server/utils/file_watcher.py b/server/utils/file_watcher.py index c942ed84..8ac5980b 100644 --- a/server/utils/file_watcher.py +++ b/server/utils/file_watcher.py @@ -6,7 +6,6 @@ class FileWatcher: - def __init__(self, file_path, callback, poll_interval=500): if not os.path.isfile(file_path): raise RuntimeError(f"Path {file_path} does not exist") @@ -31,7 +30,6 @@ def update_m_time(self): class IOLoopFileWatcher(FileWatcher): - def __init__(self, file_path, callback, poll_interval=500): if not IOLoop.current(): raise RuntimeError("No IOLoop found!") @@ -48,7 +46,7 @@ def _poll_file(self): raise IOError(f"File {self._file_path} could not be found") get_logger().warning( - f"File {self._file_path} could not be found, calling error " f"callback" + f"File {self._file_path} could not be found, calling error callback" ) self.stop() self._error_callback() diff --git a/server/utils/module_discovery.py b/server/utils/module_discovery.py index 491b4f30..171c3a98 100644 --- a/server/utils/module_discovery.py +++ b/server/utils/module_discovery.py @@ -5,6 +5,7 @@ from digi_server.logger import get_logger from utils.pkg_utils import find_end_modules + # Check if running in PyInstaller bundle try: # pylint: disable=unused-import diff --git a/server/utils/web/base_controller.py b/server/utils/web/base_controller.py index 77b8378a..94349637 100644 --- a/server/utils/web/base_controller.py +++ b/server/utils/web/base_controller.py @@ -15,12 +15,12 @@ from rbac.role import Role from schemas.schemas import ShowSchema, UserSchema + if TYPE_CHECKING: from digi_server.app_server import DigiScriptServer class BaseController(SessionMixin, RequestHandler): - def __init__( self, application: DigiScriptServer, @@ -110,7 +110,6 @@ def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]: class BaseAPIController(BaseController): - def _unimplemented_method(self, *args: str, **kwargs: str) -> None: self.set_status(405) self.write({"message": "405 not allowed"}) @@ -125,8 +124,6 @@ def on_finish(self): ) except BaseException: get_logger().debug( - f"{self.request.method} " - f"{self.request.path} " - f"{self.request.body}" + f"{self.request.method} {self.request.path} {self.request.body}" ) super().on_finish() diff --git a/server/utils/web/route.py b/server/utils/web/route.py index 607d9924..2bdb7bfd 100644 --- a/server/utils/web/route.py +++ b/server/utils/web/route.py @@ -57,7 +57,7 @@ class ApiVersion(Enum): class ApiRoute(Route): def __init__(self, route: str, api_version: ApiVersion, name: str = None): - route = f'/api/v{api_version.value}/{route.removeprefix("/")}' + route = f"/api/v{api_version.value}/{route.removeprefix('/')}" super().__init__(route, name) def __call__(self, controller): From 7cfa1172e5ec315ac5559634989da6f8c61f7822 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Mon, 1 Dec 2025 22:00:28 +0000 Subject: [PATCH 4/7] fix(client): Apply strikethrough styling to cut stage directions in cue editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #691 Cut stage directions were not showing with strikethrough styling in the cue editor view, while regular cut line parts were correctly styled. Changes: - Applied the existing 'cut-line-part' CSS class to stage directions when they are cut (checked via linePartCuts array) - Updated both the main cue editor view and the modal dialog view to consistently apply the styling The fix uses the same logic already used for regular line parts: :class="{'cut-line-part': linePartCuts.indexOf(line.line_parts[0].id) !== -1}" Since stage directions have only one line part (index 0), we check if that line part's ID is in the linePartCuts array and apply the strikethrough class accordingly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/vue_components/show/config/cues/ScriptLineCueEditor.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/vue_components/show/config/cues/ScriptLineCueEditor.vue b/client/src/vue_components/show/config/cues/ScriptLineCueEditor.vue index 2d1cb478..c7c526e7 100644 --- a/client/src/vue_components/show/config/cues/ScriptLineCueEditor.vue +++ b/client/src/vue_components/show/config/cues/ScriptLineCueEditor.vue @@ -46,6 +46,7 @@ >