diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 34f00a2b..7eb8db8a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,69 @@ +# Reconsider Dependabot if they ever add grouped updates: https://github.com/dependabot/dependabot-core/issues/1190 + +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + version: 2 updates: - package-ecosystem: "npm" - directory: "/**" - allow: - - dependency-type: "production" + directory: "/tournament-scheduler" + target-branch: "develop" + schedule: + interval: "weekly" + labels: + - "dependencies" + - "tournament-scheduler" + assignees: + - "Avasam" + reviewers: + - "Avasam" + ignore: + - dependency-name: "*" + # Temporarily adding version-update:semver-minor to the ignore as dependabot + # still wants to increase the minor version in the lockfile only despite + # versioning-strategy: increase-if-necessary. This is a know issue: + # https://github.com/dependabot/dependabot-core/issues/3891 + update-types: + ["version-update:semver-patch", "version-update:semver-minor"] + versioning-strategy: increase-if-necessary + + - package-ecosystem: "npm" + directory: "/global-scoreboard" + target-branch: "develop" + schedule: + interval: "weekly" + labels: + - "dependencies" + assignees: + - "Avasam" + reviewers: + - "Avasam" + ignore: + - dependency-name: "*" + # Temporarily adding version-update:semver-minor to the ignore as dependabot + # still wants to increase the minor version in the lockfile only despite + # versioning-strategy: increase-if-necessary. This is a know issue: + # https://github.com/dependabot/dependabot-core/issues/3891 + update-types: + ["version-update:semver-patch", "version-update:semver-minor"] + versioning-strategy: increase-if-necessary + + - package-ecosystem: "pip" + directory: "/scripts" + target-branch: "develop" + schedule: + interval: "weekly" + labels: + - "dependencies" + assignees: + - "Avasam" + reviewers: + - "Avasam" + + # Check for updates to GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/disabled-dependabot.yml b/.github/disabled-dependabot.yml deleted file mode 100644 index e5808d5f..00000000 --- a/.github/disabled-dependabot.yml +++ /dev/null @@ -1,57 +0,0 @@ -# Reconsider Dependabot if they ever add grouped updates: https://github.com/dependabot/dependabot-core/issues/1190 - -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - -version: 2 -updates: - - package-ecosystem: "npm" - directory: "/tournament-scheduler" - target-branch: "develop" - open-pull-requests-limit: 10 - schedule: - interval: "weekly" - labels: - - "dependencies" - - "tournament-scheduler" - assignees: - - "Avasam" - reviewers: - - "Avasam" - ignore: - - dependency-name: "*" - # Temporarily adding version-update:semver-minor to the ignore as dependabot - # still wants to increase the minor version in the lockfile only despite - # versioning-strategy: increase-if-necessary. This is a know issue: - # https://github.com/dependabot/dependabot-core/issues/3891 - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - versioning-strategy: increase-if-necessary - - - package-ecosystem: "npm" - directory: "/global-scoreboard" - target-branch: "develop" - open-pull-requests-limit: 10 - schedule: - interval: "weekly" - labels: - - "dependencies" - assignees: - - "Avasam" - reviewers: - - "Avasam" - ignore: - - dependency-name: "*" - # Temporarily adding version-update:semver-minor to the ignore as dependabot - # still wants to increase the minor version in the lockfile only despite - # versioning-strategy: increase-if-necessary. This is a know issue: - # https://github.com/dependabot/dependabot-core/issues/3891 - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - versioning-strategy: increase-if-necessary - - # Check for updates to GitHub Actions - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index bd56b631..b07360b2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,12 +13,12 @@ name: "CodeQL" on: push: - branches: [ main, develop ] + branches: [main, develop] pull_request: # The branches below must be a subset of the branches above - branches: [ develop ] + branches: [develop] schedule: - - cron: '26 13 * * 6' + - cron: "26 13 * * 6" jobs: analyze: @@ -28,40 +28,40 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'javascript', 'python' ] + language: ["javascript", "python"] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - - name: Checkout repository - uses: actions/checkout@v3 + - name: Checkout repository + uses: actions/checkout@v3 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl - # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language - #- run: | - # make bootstrap - # make release + #- run: | + # make bootstrap + # make release - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/lint-and-build-global-scoreboard.yml b/.github/workflows/lint-and-build-global-scoreboard.yml index 3e5fb3bc..7795cc5e 100644 --- a/.github/workflows/lint-and-build-global-scoreboard.yml +++ b/.github/workflows/lint-and-build-global-scoreboard.yml @@ -4,14 +4,17 @@ on: push: branches: - main + - develop paths: - - 'global-scoreboard/**' + - "global-scoreboard/**" + - ".github/workflows/lint-and-build-global-scoreboard.yml" pull_request: branches: - main - develop paths: - - 'global-scoreboard/**' + - "global-scoreboard/**" + - ".github/workflows/lint-and-build-global-scoreboard.yml" jobs: ESLint: runs-on: ubuntu-latest diff --git a/.github/workflows/lint-and-build-tournament-scheduler.yml b/.github/workflows/lint-and-build-tournament-scheduler.yml index b44c2a56..0135a886 100644 --- a/.github/workflows/lint-and-build-tournament-scheduler.yml +++ b/.github/workflows/lint-and-build-tournament-scheduler.yml @@ -4,14 +4,17 @@ on: push: branches: - main + - develop paths: - - 'tournament-scheduler/**' + - "tournament-scheduler/**" + - ".github/workflows/lint-and-build-tournament-scheduler.yml" pull_request: branches: - main - develop paths: - - 'tournament-scheduler/**' + - "tournament-scheduler/**" + - ".github/workflows/lint-and-build-tournament-scheduler.yml" jobs: ESLint: runs-on: ubuntu-latest diff --git a/.github/workflows/lint-python.yml b/.github/workflows/lint-python.yml index 0dacd4c5..1d90ce3e 100644 --- a/.github/workflows/lint-python.yml +++ b/.github/workflows/lint-python.yml @@ -4,9 +4,12 @@ on: push: branches: - main + - develop paths: - "**.py" - "**.pyi" + - ".github/workflows/lint-python.yml" + - "scripts/requirements*.txt" pull_request: branches: - main @@ -14,13 +17,19 @@ on: paths: - "**.py" - "**.pyi" + - ".github/workflows/lint-python.yml" + - "scripts/requirements*.txt" env: + # https://help.pythonanywhere.com/pages/ChangingSystemImage/#available-python-versions-for-system-images python-version: "3.10" + # https://help.pythonanywhere.com/pages/ChangingSystemImage/#base-ubuntu-version-for-each-system-image + # runs-on: "ubuntu-20.04" jobs: + # Jobs that don't need dependencies installed isort: - runs-on: ubuntu-latest + runs-on: "ubuntu-20.04" steps: - name: Checkout ${{ github.repository }}/${{ github.ref }} uses: actions/checkout@v3 @@ -29,15 +38,11 @@ jobs: with: python-version: ${{ env.python-version }} cache: "pip" - cache-dependency-path: "scripts/requirements*.txt" - - name: Install dependencies - run: | - pip install wheel - pip install -r "scripts/requirements.txt" - - name: Analysing the code with ${{ job.name }} - run: isort backend/ typings/ --check-only + cache-dependency-path: "scripts/requirements-dev.txt" + - run: pip install isort + - run: isort backend/ typings/ --check-only add-trailing-comma: - runs-on: ubuntu-latest + runs-on: "ubuntu-20.04" steps: - name: Checkout ${{ github.repository }}/${{ github.ref }} uses: actions/checkout@v3 @@ -46,12 +51,11 @@ jobs: with: python-version: ${{ env.python-version }} cache: "pip" - cache-dependency-path: "scripts/requirements*.txt" + cache-dependency-path: "scripts/requirements-dev.txt" - run: pip install add-trailing-comma - - name: Analysing the code with ${{ job.name }} - run: add-trailing-comma $(git ls-files '**.py*') --py36-plus + - run: add-trailing-comma $(git ls-files '**.py*') --py36-plus Bandit: - runs-on: ubuntu-latest + runs-on: "ubuntu-20.04" steps: - name: Checkout ${{ github.repository }}/${{ github.ref }} uses: actions/checkout@v3 @@ -60,13 +64,13 @@ jobs: with: python-version: ${{ env.python-version }} cache: "pip" - cache-dependency-path: "scripts/requirements*.txt" + cache-dependency-path: "scripts/requirements-dev.txt" - run: pip install bandit - - run: mv backend/configs.template.py backend/configs.py - - name: Analysing the code with ${{ job.name }} - run: bandit -n 1 --severity-level medium --recursive backend + - run: bandit -n 1 --severity-level medium --recursive backend + + # Jobs that do need dependencies installed Pyright: - runs-on: ubuntu-latest + runs-on: "ubuntu-20.04" steps: - name: Checkout ${{ github.repository }}/${{ github.ref }} uses: actions/checkout@v3 @@ -79,15 +83,14 @@ jobs: - name: Install dependencies run: | pip install wheel - pip install -r "scripts/requirements.txt" + pip install -r "scripts/requirements-dev.txt" - run: mv backend/configs.template.py backend/configs.py - - name: Analysing the code with ${{ job.name }} - uses: jakebailey/pyright-action@v1 + - uses: jakebailey/pyright-action@v1 with: working-directory: backend/ extra-args: --warnings Pylint: - runs-on: ubuntu-latest + runs-on: "ubuntu-20.04" steps: - name: Checkout ${{ github.repository }}/${{ github.ref }} uses: actions/checkout@v3 @@ -100,12 +103,11 @@ jobs: - name: Install dependencies run: | pip install wheel - pip install -r "scripts/requirements.txt" + pip install -r "scripts/requirements-dev.txt" - run: mv backend/configs.template.py backend/configs.py - - name: Analysing the code with ${{ job.name }} - run: pylint backend/ --reports=y --output-format=colorized + - run: pylint backend/ --reports=y --output-format=colorized Flake8: - runs-on: ubuntu-latest + runs-on: "ubuntu-20.04" steps: - name: Checkout ${{ github.repository }}/${{ github.ref }} uses: actions/checkout@v3 @@ -114,11 +116,6 @@ jobs: with: python-version: ${{ env.python-version }} cache: "pip" - cache-dependency-path: "scripts/requirements*.txt" - - name: Install dependencies - run: | - pip install wheel - pip install -r "scripts/requirements.txt" - - run: mv backend/configs.template.py backend/configs.py - - name: Analysing the code with ${{ job.name }} - run: flake8 backend/ typings/ + cache-dependency-path: "scripts/requirements-dev.txt" + - run: pip install -r "scripts/requirements-dev.txt" + - run: flake8 backend/ typings/ diff --git a/.vscode/launch.json b/.vscode/launch.json index fda2eb27..3e6da7db 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ "module": "flask", "env": { "FLASK_APP": "flask_app.py", - "FLASK_ENV": "development" + "FLASK_DEBUG": "true" }, "args": [ "run", diff --git a/.vscode/settings.json b/.vscode/settings.json index 81288b11..ad24e539 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,14 @@ "files.insertFinalNewline": true, "files.trimFinalNewlines": true, "files.trimTrailingWhitespace": true, + "[markdown]": { + "files.trimTrailingWhitespace": false, + }, + "trailing-spaces.includeEmptyLines": true, + "trailing-spaces.trimOnSave": true, + "trailing-spaces.syntaxIgnore": [ + "markdown" + ], "editor.comments.insertSpace": true, "editor.insertSpaces": true, "editor.detectIndentation": false, @@ -23,11 +31,6 @@ "source.fixAll.convertImportFormat": true, "source.organizeImports": false, }, - "trailing-spaces.includeEmptyLines": true, - "trailing-spaces.trimOnSave": true, - "trailing-spaces.syntaxIgnore": [ - "markdown" - ], "emeraldwalk.runonsave": { "commands": [ { diff --git a/README.md b/README.md index 194c31a0..88f32964 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,8 @@ Note: The soft cutoff works great on games such as Barney. But is too punishing ## Dev environment setup -Get yourself a [MySQL server](https://dev.mysql.com/downloads/mysql/) (as of 2021/06/01, PythonAnywhere uses version 5.7.27) -Install [Python](https://www.python.org/downloads/) 3.7 or above (validated up to 3.9) +Get yourself a [MySQL server](https://dev.mysql.com/downloads/mysql/) (as of 2022/13/08, PythonAnywhere uses version 8.0.25, MySQL Server 5.7.34) +Install [Python](https://www.python.org/downloads/) 3.9 or above (PythonAnywhere runs on 3.10) Run `./scripts/install.bat` to install the required dependencies. Copy `configs.template.py` as `configs.py` and update the file as needed. If needed, copy `.env.development` as `.env.development.local` and update the file. diff --git a/backend/api/api_wrappers.py b/backend/api/api_wrappers.py index 5fb2161d..a01700fc 100644 --- a/backend/api/api_wrappers.py +++ b/backend/api/api_wrappers.py @@ -1,12 +1,16 @@ +from __future__ import annotations + import json from datetime import datetime, timedelta from functools import wraps -from typing import Union import jwt from flask import current_app, jsonify, request from models.core_models import Player +# TODO: Validate and maybe fix stubs +# pyright: reportOptionalMemberAccess=false + def authentication_required(fn): @wraps(fn) @@ -45,7 +49,7 @@ def _verify(*args, **kwargs): if not isinstance(response, tuple) or not isinstance(response[0], str): return response - extended_token: Union[bytes, str] = jwt.encode( + extended_token: bytes | str = jwt.encode( { "sub": data["sub"], "iat": data["iat"], diff --git a/backend/api/game_search_api.py b/backend/api/game_search_api.py index 9ebaa678..633a4222 100644 --- a/backend/api/game_search_api.py +++ b/backend/api/game_search_api.py @@ -11,4 +11,4 @@ @api.route("/game-values", methods=("GET",)) def get_all_game_values(): - return jsonify(map_to_dto(GameValues.query.all())) + return jsonify(map_to_dto(GameValues.query.all())) # pyright: ignore[reportOptionalMemberAccess] diff --git a/backend/api/global_scoreboard_api.py b/backend/api/global_scoreboard_api.py index d97a29ed..767522e6 100644 --- a/backend/api/global_scoreboard_api.py +++ b/backend/api/global_scoreboard_api.py @@ -35,7 +35,7 @@ def get_all_players(): def get_player_score_details(user_id: str): player = Player.get(user_id) if player: - return player.score_details or "" + return str(player.score_details) or "" return "", 404 @@ -45,7 +45,7 @@ def update_player(name_or_id: str): if configs.bypass_update_restrictions: return __do_update_player_bypass_restrictions(name_or_id) # pylint: disable=no-value-for-parameter # TODO: Raise this issue upstream - return __do_update_player(name_or_id) # type: ignore # TODO: Raise this issue upstream + return __do_update_player(name_or_id) # pyright: ignore[reportGeneralTypeIssues] except UserUpdaterError as exception: error_message = f"Error: {exception.args[0]['error']}\n{exception.args[0]['details']}" return error_message, 424 diff --git a/backend/models/core_models.py b/backend/models/core_models.py index 36887370..7d138e13 100644 --- a/backend/models/core_models.py +++ b/backend/models/core_models.py @@ -6,7 +6,7 @@ from datetime import datetime from typing import TYPE_CHECKING, Any, TypedDict, Union, cast -from flask_sqlalchemy import SQLAlchemy +from flask_sqlalchemy import Model, SQLAlchemy # pylint: disable=no-name-in-module from models.exceptions import SpeedrunComError, UserUpdaterError from models.src_dto import SrcProfileDto from services.utils import get_file @@ -21,22 +21,20 @@ db = SQLAlchemy() -if TYPE_CHECKING: - from flask_sqlalchemy.model import Model - BaseModel = db.make_declarative_base(Model) -else: - BaseModel = db.Model +BaseModel: type[Model] = db.Model # pyright: ignore[reportGeneralTypeIssues] +# TODO: Validate and maybe fix stubs +# pyright: reportOptionalMemberAccess=false friend = db.Table( "friend", - db.Column( + Column( "user_id", - db.String(8), + String(8), db.ForeignKey("player.user_id"), ), - db.Column( + Column( "friend_id", - db.String(8), + String(8), db.ForeignKey("player.user_id"), ), ) @@ -58,13 +56,13 @@ class ScheduleOrderDict(TypedDict): class Player(BaseModel): __tablename__ = "player" - user_id = db.Column(db.String(8), primary_key=True) - name = db.Column(db.String(32), nullable=False) + user_id = Column(String(8), primary_key=True) + name = Column(String(32), nullable=False) # The biggest region code I found so far was "us/co/coloradosprings" at 21 - country_code = db.Column(db.String(24)) - score = db.Column(db.Integer, nullable=False) - score_details = db.Column(db.String()) - last_update = db.Column(db.DateTime()) + country_code = Column(String(24)) + score = Column(Integer, nullable=False) + score_details = Column(String()) + last_update = Column(DateTime()) rank: int | None = None schedules = db.relationship("Schedule", back_populates="owner") @@ -312,15 +310,19 @@ def update_schedule( for existing_time_slot in schedule_to_update.time_slots: if time_slot_to_edit["id"] == existing_time_slot.time_slot_id: new_time_slot = existing_time_slot + new_time_slot.date_time = datetime.strptime(time_slot_to_edit["dateTime"], DATETIME_FORMAT) + new_time_slot.maximum_entries = time_slot_to_edit["maximumEntries"] + new_time_slot.participants_per_entry = time_slot_to_edit["participantsPerEntry"] + new_time_slot.schedule_id = schedule_id break # ... otherwise, create a brand new TimeSlot else: - new_time_slot = TimeSlot() # type: ignore - # Do the necessary modifications - new_time_slot.schedule_id = schedule_id - new_time_slot.date_time = datetime.strptime(time_slot_to_edit["dateTime"], DATETIME_FORMAT) - new_time_slot.maximum_entries = time_slot_to_edit["maximumEntries"] - new_time_slot.participants_per_entry = time_slot_to_edit["participantsPerEntry"] + new_time_slot = TimeSlot( + date_time=datetime.strptime(time_slot_to_edit["dateTime"], DATETIME_FORMAT), + maximum_entries=time_slot_to_edit["maximumEntries"], + participants_per_entry=time_slot_to_edit["participantsPerEntry"], + schedule_id=schedule_id, + ) new_time_slots.append(new_time_slot) diff --git a/backend/models/game_search_models.py b/backend/models/game_search_models.py index 789646d8..9f12a764 100644 --- a/backend/models/game_search_models.py +++ b/backend/models/game_search_models.py @@ -9,14 +9,14 @@ class GameValues(BaseModel): __tablename__ = "game_values" - game_id = db.Column(db.String(8), primary_key=True) - category_id = db.Column(db.String(8), primary_key=True) - run_id = db.Column(db.String(8), nullable=False) - platform_id = db.Column(db.String(8)) - alternate_platforms = db.Column(db.String()) - wr_time = db.Column(db.Integer, nullable=False) - wr_points = db.Column(db.Integer, nullable=False) - mean_time = db.Column(db.Integer, nullable=False) + game_id = Column(String(8), primary_key=True) + category_id = Column(String(8), primary_key=True) + run_id = Column(String(8), nullable=False) + platform_id = Column(String(8)) + alternate_platforms = Column(String()) + wr_time = Column(Integer, nullable=False) + wr_points = Column(Integer, nullable=False) + mean_time = Column(Integer, nullable=False) if TYPE_CHECKING: # noqa: CCE002 def __init__( # pylint: disable=too-many-arguments @@ -96,7 +96,7 @@ def get(game_id: str, category_id: str): GameValues, GameValues .query - .filter(GameValues.game_id == game_id) + .filter(GameValues.game_id == game_id) # pyright: ignore[reportOptionalMemberAccess] .filter(GameValues.category_id == category_id) .one(), ) diff --git a/backend/models/global_scoreboard_models.py b/backend/models/global_scoreboard_models.py index 1c238a46..51ab4a2e 100644 --- a/backend/models/global_scoreboard_models.py +++ b/backend/models/global_scoreboard_models.py @@ -127,7 +127,7 @@ def get_points_distribution_dto(self) -> PointsDistributionDto: ] def fetch_and_set_user_code_and_name(self) -> None: - url = "https://www.speedrun.com/api/v1/users/{user}".format(user=self._id) + url = "https://www.speedrun.com/api/v1/users/{user}".format(user=self._id) # pylint: disable=C0209 infos: SrcProfileDto = get_file(url, {}, "http_cache")["data"] self._id = infos["id"] diff --git a/backend/models/tournament_scheduler_models.py b/backend/models/tournament_scheduler_models.py index 5df82291..d9b4fcb7 100644 --- a/backend/models/tournament_scheduler_models.py +++ b/backend/models/tournament_scheduler_models.py @@ -7,18 +7,21 @@ from services.utils import map_to_dto from sqlalchemy import Boolean, Column, DateTime, Integer, String, orm +# TODO: Validate and maybe fix stubs +# pyright: reportOptionalMemberAccess=false + CASCADE = "all,delete,delete-orphan" class Schedule(BaseModel): __tablename__ = "schedule" - schedule_id = db.Column(db.Integer, primary_key=True) - name: str | Column[String] = db.Column(db.String(128), nullable=False, default="") - owner_id = db.Column(db.String(8), db.ForeignKey("player.user_id"), nullable=False) - registration_key = db.Column(db.String(36), nullable=False) - is_active: bool | Column[Boolean] = db.Column(db.Boolean, nullable=False, default=True) - deadline: Optional[datetime | Column[DateTime]] = db.Column(db.DateTime, nullable=True) + schedule_id = Column(Integer, primary_key=True) + name: str | Column[String] = Column(String(128), nullable=False, default="") + owner_id = Column(String(8), db.ForeignKey("player.user_id"), nullable=False) + registration_key = Column(String(36), nullable=False) + is_active: bool | Column[Boolean] = Column(db.Boolean, nullable=False, default=True) + deadline: datetime | Column[DateTime] | None = Column(DateTime, nullable=True) owner: Player = db.relationship("Player", back_populates="schedules") time_slots: list[TimeSlot] = db.relationship( @@ -27,8 +30,8 @@ class Schedule(BaseModel): back_populates="schedule", ) - group_id: int | Column[Integer] | None = db.Column(db.Integer, nullable=True) - order: int | Column[Integer] | None = db.Column(db.Integer, nullable=False, default=-1) + group_id: int | Column[Integer] | None = Column(Integer, nullable=True) + order: int | Column[Integer] | None = Column(Integer, nullable=False, default=-1) if TYPE_CHECKING: # noqa: CCE002 def __init__( # pylint: disable=too-many-arguments @@ -40,8 +43,8 @@ def __init__( # pylint: disable=too-many-arguments time_slots: list[TimeSlot] = ..., group_id: int | Column[Integer] | None = ..., name: str | Column[String] | None = ..., - is_active: Optional[bool | Column[Boolean]] = ..., - deadline: Optional[datetime | Column[DateTime]] = ..., + is_active: bool | Column[Boolean] | None = ..., + deadline: datetime | Column[DateTime] | None = ..., order: int | Column[Integer] | None = ..., ): ... @@ -80,10 +83,10 @@ def to_dto(self): class ScheduleGroup(BaseModel): __tablename__ = "schedule_group" - group_id = db.Column(db.Integer, primary_key=True) - name: str | Column[String] = db.Column(db.String(128), nullable=False, default="") - owner_id = db.Column(db.String(8), db.ForeignKey("player.user_id"), nullable=False) - order: int | Column[Integer] | None = db.Column(db.Integer, nullable=False, default=-1) + group_id = Column(Integer, primary_key=True) + name: str | Column[String] = Column(String(128), nullable=False, default="") + owner_id = Column(String(8), db.ForeignKey("player.user_id"), nullable=False) + order: int | Column[Integer] | None = Column(Integer, nullable=False, default=-1) if TYPE_CHECKING: # noqa: CCE002 def __init__( # pylint: disable=too-many-arguments @@ -117,11 +120,11 @@ def to_dto(self): class TimeSlot(BaseModel): __tablename__ = "time_slot" - time_slot_id = db.Column(db.Integer, primary_key=True) - schedule_id: int | Column[Integer] = db.Column(db.Integer, db.ForeignKey("schedule.schedule_id"), nullable=False) - date_time: datetime | Column[DateTime] = db.Column(db.DateTime, nullable=False) - maximum_entries: int | Column[Integer] = db.Column(db.Integer, nullable=False) - participants_per_entry: int | Column[Integer] = db.Column(db.Integer, nullable=False) + time_slot_id = Column(Integer, primary_key=True) + schedule_id: int | Column[Integer] = Column(Integer, db.ForeignKey("schedule.schedule_id"), nullable=False) + date_time: datetime | Column[DateTime] = Column(DateTime, nullable=False) + maximum_entries: int | Column[Integer] = Column(Integer, nullable=False) + participants_per_entry: int | Column[Integer] = Column(Integer, nullable=False) schedule: Schedule = db.relationship("Schedule", back_populates="time_slots") registrations: list[Registration] = db.relationship( @@ -144,7 +147,7 @@ def __init__( # pylint: disable=too-many-arguments ... @ staticmethod - def get_with_key(time_slot_id: int, registration_key: str) -> Optional[TimeSlot]: + def get_with_key(time_slot_id: int, registration_key: str) -> TimeSlot | None: try: parent_schedule = cast( Schedule, @@ -195,8 +198,8 @@ def to_dto(self): class Registration(BaseModel): __tablename__ = "registration" - registration_id = db.Column(db.Integer, primary_key=True) - time_slot_id = db.Column(db.Integer, db.ForeignKey("time_slot.time_slot_id"), nullable=False) + registration_id = Column(Integer, primary_key=True) + time_slot_id = Column(Integer, db.ForeignKey("time_slot.time_slot_id"), nullable=False) timeslot: TimeSlot = db.relationship("TimeSlot", back_populates="registrations") participants: list[Participant] = db.relationship( @@ -218,17 +221,18 @@ def __init__( # pylint: disable=too-many-arguments def to_dto(self): return { "id": self.registration_id, - "participants": [participant.name for participant in self.participants], + # TODO: Report false-positive + "participants": [participant.name for participant in self.participants], # pylint: disable=E1133 } class Participant(BaseModel): __tablename__ = "participant" - registration_id: int | Column[Integer] = db.Column( - db.Integer, db.ForeignKey("registration.registration_id"), primary_key=True, + registration_id: int | Column[Integer] = Column( + Integer, db.ForeignKey("registration.registration_id"), primary_key=True, ) - name: str | Column[String] = db.Column(db.String(128), primary_key=True) + name: str | Column[String] = Column(String(128), primary_key=True) registration: Registration = db.relationship("Registration", back_populates="participants") diff --git a/backend/services/cached_requests.py b/backend/services/cached_requests.py index 8fffb7ce..94459d12 100644 --- a/backend/services/cached_requests.py +++ b/backend/services/cached_requests.py @@ -1,6 +1,8 @@ +from __future__ import annotations + import sys from datetime import timedelta -from typing import Literal, Union, cast +from typing import Literal, cast import configs from requests import Session @@ -58,7 +60,7 @@ def __make_cache_session(user_id: str = "http_cache"): return session -def use_session(user_id: Union[str, Literal[False]] = "http_cache"): +def use_session(user_id: str | Literal[False] = "http_cache"): """ @param user_id: User specific cache. Omit or "http_cache" for global. False for uncached. """ diff --git a/backend/services/user_updater.py b/backend/services/user_updater.py index 4af8e848..8fd2d6d5 100644 --- a/backend/services/user_updater.py +++ b/backend/services/user_updater.py @@ -1,9 +1,11 @@ +from __future__ import annotations + from datetime import datetime from math import exp, floor, pi from re import sub from sqlite3 import OperationalError from time import strftime -from typing import Union +from typing import cast from urllib.parse import unquote import configs @@ -25,7 +27,7 @@ TIME_BONUS_DIVISOR = 3600 * 12 # 12h (1/2 day) for +100% -def get_updated_user(user_id: str) -> dict[str, Union[str, None, float, int, PointsDistributionDto]]: +def get_updated_user(user_id: str) -> dict[str, str | float | int | PointsDistributionDto | None]: """Called from flask_app and AutoUpdateUsers.run()""" text_output: str = user_id result_state: str = "info" @@ -65,7 +67,7 @@ def get_updated_user(user_id: str) -> dict[str, Union[str, None, float, int, Poi if ( not player or not player.last_update - or (datetime.utcnow() - player.last_update).days >= configs.last_updated_days[0] + or (datetime.utcnow() - cast(datetime, player.last_update)).days >= configs.last_updated_days[0] or configs.bypass_update_restrictions ): __set_user_points(user) diff --git a/backend/services/user_updater_helpers.py b/backend/services/user_updater_helpers.py index 02709f13..decaca09 100644 --- a/backend/services/user_updater_helpers.py +++ b/backend/services/user_updater_helpers.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import json import math from math import floor from time import strftime -from typing import Any, Optional +from typing import Any from models.core_models import Player from models.global_scoreboard_models import Run, User @@ -226,7 +228,7 @@ def is_top_run(run: Run): # Check if it's possible to replace a handful of ILs by a full run, starting backward. # This can happen when there's not enough ILs to fill in for the weight of a full run. # See: https://github.com/Avasam/speedrun.com_global_scoreboard_webapp/issues/174 - first_lesser_full_game: Optional[Run] = next((run for run in lesser_runs if run.level_fraction == 1), None) + first_lesser_full_game: Run | None = next((run for run in lesser_runs if run.level_fraction == 1), None) if first_lesser_full_game is not None and position < MIN_SAMPLE_SIZE: runs_to_transfer_weight = MIN_SAMPLE_SIZE - position runs_to_transfer_reversed: list[Run] = [] diff --git a/backend/services/utils.py b/backend/services/utils.py index 30ffe461..39fa8021 100644 --- a/backend/services/utils.py +++ b/backend/services/utils.py @@ -9,7 +9,7 @@ from random import randint from sqlite3 import OperationalError from time import sleep -from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast +from typing import TYPE_CHECKING, Any, Literal, cast from urllib.parse import parse_qs, urlparse import configs @@ -65,7 +65,7 @@ def __handle_json_error(response: Response, json_exception: ValueError): }) from json_exception -def __handle_json_data(json_data: SrcErrorResultDto, response_status_code: int) -> Optional[SrcDataResultDto]: +def __handle_json_data(json_data: SrcErrorResultDto, response_status_code: int) -> SrcDataResultDto | None: if "status" not in json_data: return json_data @@ -91,9 +91,9 @@ def __handle_json_data(json_data: SrcErrorResultDto, response_status_code: int) def __get_request_cache_bust_if_disk_quota_exceeded( url: str, - params: Optional[_Params], - cached: Union[str, Literal[False]], - headers: Optional[dict[str, Any]], + params: _Params | None, + cached: str | Literal[False], + headers: dict[str, Any] | None, ): try: response = use_session(cached).get(url, params=params, headers=headers) @@ -125,9 +125,9 @@ def __get_request_cache_bust_if_disk_quota_exceeded( def get_file( url: str, - params: Optional[_Params] = None, - cached: Union[str, Literal[False]] = False, - headers: Optional[dict[str, Any]] = None, + params: _Params | None = None, + cached: str | Literal[False] = False, + headers: dict[str, Any] | None = None, ) -> SrcDataResultDto: """ Returns the content of "url" parsed as JSON dict. @@ -163,13 +163,13 @@ def get_file( def params_from_url(url: str): if not url: return {} - params: dict[str, Union[str, int]] = {k: v[0] for k, v in parse_qs(urlparse(url).query).items()} + params: dict[str, str | int] = {k: v[0] for k, v in parse_qs(urlparse(url).query).items()} if not params.get("max"): params["max"] = MINIMUM_RESULTS_PER_PAGE return params -def get_paginated_response(url: str, params: dict[str, Union[str, int]], related_user_id: str): +def get_paginated_response(url: str, params: dict[str, str | int], related_user_id: str): next_params = params if not next_params.get("max"): next_params["max"] = MINIMUM_RESULTS_PER_PAGE @@ -177,7 +177,7 @@ def get_paginated_response(url: str, params: dict[str, Union[str, int]], related summed_results: SrcPaginatedDataResultDto = {"data": []} results_per_page = initial_results_per_page = int(next_params["max"]) - def update_next_params(new_results_per_page: Optional[int] = None, take_next: bool = True): + def update_next_params(new_results_per_page: int | None = None, take_next: bool = True): nonlocal next_params nonlocal results_per_page pagination_max = int(next_params["max"]) @@ -232,11 +232,11 @@ def parse_str_to_bool(string_to_parse: str | None) -> bool: return string_to_parse is not None and string_to_parse.lower() == "true" -def parse_str_to_nullable_bool(string_to_parse: str | None) -> Optional[bool]: +def parse_str_to_nullable_bool(string_to_parse: str | None) -> bool | None: return None if string_to_parse is None else string_to_parse.lower() == "true" -def map_to_dto(dto_mappable_object_list) -> list[dict[str, Union[str, bool, int]]]: +def map_to_dto(dto_mappable_object_list) -> list[dict[str, str | bool | int]]: return [dto_mappable_object.to_dto() for dto_mappable_object in dto_mappable_object_list] diff --git a/pyproject.toml b/pyproject.toml index 005c7510..1e65aded 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,8 @@ reportMissingSuperCall="none" # False positives on base classes reportPropertyTypeMismatch="error" reportUninitializedInstanceVariable="error" reportUnnecessaryTypeIgnoreComment="error" +# Use `pyright: ignore`, not `type: ignore` +enableTypeIgnoreComments=false # Ignore must be specified for Pylance to stop displaying errors ignore = [ # We expect stub files to be incomplete or contain useless statements diff --git a/scripts/Deploy_react_apps.sh b/scripts/Deploy.sh similarity index 88% rename from scripts/Deploy_react_apps.sh rename to scripts/Deploy.sh index 698dd09d..4fed68cc 100644 --- a/scripts/Deploy_react_apps.sh +++ b/scripts/Deploy.sh @@ -6,3 +6,4 @@ if [ -f global-scoreboard-build.zip ]; then rm -rfv global-scoreboard/build unzip -o global-scoreboard-build.zip -d global-scoreboard/ fi +pip install -r scripts/requirements.txt diff --git a/scripts/install.ps1 b/scripts/install.ps1 index c39ef4f6..bfdcb012 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -7,5 +7,5 @@ If ($IsWindows) { # Ensures installation tools are up to date. This also aliases pip to pip3 on MacOS. python3 -m pip install wheel pip setuptools --upgrade -pip install -r "$PSScriptRoot/requirements.txt" --upgrade +pip install -r "$PSScriptRoot/requirements-dev.txt" --upgrade npm i --global pyright@latest diff --git a/scripts/requirements-dev.txt b/scripts/requirements-dev.txt new file mode 100644 index 00000000..b0190dde --- /dev/null +++ b/scripts/requirements-dev.txt @@ -0,0 +1,27 @@ +-r requirements.txt + +# Linters +bandit +flake8>=5,<6 # flake8-pyi deprecation warnings # flake8-quotes doesn't support v6 yet +flake8-builtins +flake8-bugbear +flake8-class-attributes-order +flake8-comprehensions>=3.8 # flake8 5 support +flake8-datetimez +flake8-pyi>=22.11.0 # flake8 6 support +flake8-quotes +flake8-simplify +pep8-naming +pylint>=2.14,<2.16.0 # New checks # 2.16 still in pre-release, and older +pylint-flask +pylint-flask-sqlalchemy +# Formatters +add-trailing-comma>=2.3.0 # Added support for with statement +autopep8>=2.0.0 # New checks +isort +unify + +# types +types-Flask-SQLAlchemy +types-httplib2 +types-requests diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 1c1de14b..f2319f67 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -1,37 +1,16 @@ -flask -flask_sqlalchemy -httplib2 -mysql-connector -pyjwt +# Pythonanywhere pre-installed +# https://www.pythonanywhere.com/batteries_included/ +# Set image to "haggis" and Python version to "3.10" +Flask>=2.1.2 +Flask-SQLAlchemy>=2.5.1 +httplib2>=0.20.4 +# Stuck on this version until I can figure out how to tell the engine to use collation=utf8_general_ci, and/or fix the charset 255 issue +mysql-connector-python<=8.0.16 +# mysql-connector-python>=8.0.29 +PyJWT>=2.4.0 +requests>=2.28.1 +requests-cache>=0.9.4 +SQLAlchemy>=1.4.36 +# Custom requirements ratelimiter redislite ; sys_platform != 'win32' -requests -requests-cache>=0.9.3 -sqlalchemy - -# Linters -bandit -flake8>=5,<6 # flake8-pyi deprecation warnings # flake8-quotes doesn't support v6 yet -flake8-builtins -flake8-bugbear -flake8-class-attributes-order -flake8-comprehensions>=3.8 # flake8 5 support -flake8-datetimez -flake8-pyi>=22.11.0 # flake8 6 support -flake8-quotes -flake8-simplify -pep8-naming -pylint>=2.14,<3.0.0 # New checks # 3.0 still in pre-release -pylint-flask -pylint-flask-sqlalchemy -# Formatters -add-trailing-comma>=2.3.0 # Added support for with statement -autopep8>=2.0.0 # New checks -isort -unify - -# types -types-Flask # TODO: Deprecated, update Flask instead! -types-Flask-SQLAlchemy -types-httplib2 -types-requests