Skip to content

Commit

Permalink
Merge branch 'aerich-0.7' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
Rongronggg9 committed Apr 23, 2024
2 parents 15bea0a + f9770cd commit b5525cc
Show file tree
Hide file tree
Showing 26 changed files with 467 additions and 91 deletions.
47 changes: 46 additions & 1 deletion .github/workflows/rough-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
echo 'TABLE_TO_IMAGE=1' >> .env_sqlite
echo 'MULTIPROCESSING=1' >> .env_sqlite
cp .env_sqlite .env_postgresql
echo 'DATABASE_URL=postgresql://test:test@localhost:5432/test' >> .env_postgresql
echo 'DATABASE_URL=postgres://test:test@localhost:5432/test' >> .env_postgresql
ln -s src rsstt
- name: Test RSS-to-Telegram-Bot (SQLite)
shell: bash
Expand All @@ -78,3 +78,48 @@ jobs:
run: |
set -eux
python3 -m rsstt
- name: Test scripts/aerich_helper.py (SQLite)
shell: bash
run: |
set -eux
./scripts/aerich_helper.py -h
./scripts/aerich_helper.py -v history
./scripts/aerich_helper.py -v heads
./scripts/aerich_helper.py -v upgrade True
- name: Test scripts/aerich_helper.py (PostgreSQL)
if: matrix.os == 'ubuntu-latest'
shell: bash
run: |
set -eux
. .env_postgresql
./scripts/aerich_helper.py -v -u "$DATABASE_URL" history
./scripts/aerich_helper.py -v -u "$DATABASE_URL" heads
./scripts/aerich_helper.py -v -u "$DATABASE_URL" upgrade True
- name: Test aerich migration upgrade (SQLite)
shell: bash
run: |
set -eux
rm -rf config
cp .env_sqlite .env
CURR_REF="$(git rev-parse HEAD)"
git checkout stale/aerich-0.6
pip install -qr requirements.txt
python3 -u telegramRSSbot.py
git checkout "$CURR_REF"
pip install -qr requirements.txt
python3 -u telegramRSSbot.py
- name: Test aerich migration upgrade (PostgreSQL)
if: matrix.os == 'ubuntu-latest'
shell: bash
run: |
set -eux
sudo -u postgres psql -c "DROP DATABASE test;"
sudo -u postgres psql -c "CREATE DATABASE test OWNER test;"
cp .env_postgresql .env
CURR_REF="$(git rev-parse HEAD)"
git checkout stale/aerich-0.6
pip install -qr requirements.txt
python3 -u telegramRSSbot.py
git checkout "$CURR_REF"
pip install -qr requirements.txt
python3 -u telegramRSSbot.py
12 changes: 0 additions & 12 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -217,18 +217,6 @@ crashlytics.properties
crashlytics-build.properties
fabric.properties

### VirtualEnv template
# Virtualenv
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
pip-selfcheck.json

### Linux template
*~

Expand Down
5 changes: 5 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

## Unreleased

### BREAKING CHANGES

- **Migrate to `aerich` 0.7.2**: A breaking change introduced in `aerich` (a dependency of RSStT) 0.7.x has prevented RSStT from upgrading it for a long time. A lot of effort has been made, so the migration is expected to be seamless and shouldn't break anything. However, it is encouraged to make a database backup before upgrading RSStT. If you encounter any issues due to the migration, please file a bug report.

### Highlights

- **Support Python 3.12**: Minor fixes have been made to support Python 3.12. The official Docker image is now based on Python 3.12 as well.
- **Helper scripts to make contributions easier**: When performing contributions that update database models, creating database migration files is not an easy job. [scripts/aerich_helper.py](../scripts/aerich_helper.py) is a helper script that can simplify the process. Passing `--help` to the script to see a detailed usage guide.

### Enhancements

Expand Down
5 changes: 5 additions & 0 deletions docs/CHANGELOG.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

## 未发布

### 重大变更

- **迁移到 `aerich` 0.7.2**`aerich` (RSStT 的一个依赖项) 0.7.x 中引入的一个重大变更使 RSStT 长期以来无法升级它。已经作出了很多努力,所以迁移预期为无缝的,且不应该造成任何破坏。但是,建议在升级 RSStT 之前进行数据库备份。如果您因迁移而遇到任何问题,请提交错误报告。

### 亮点

- **支持 Python 3.12**: 进行了一些小的修复以支持 Python 3.12。官方 Docker 镜像现在也基于 Python 3.12。
- **使贡献更容易的辅助脚本**: 在进行更新数据库模型的贡献时,创建数据库迁移文件并不是一件容易的事。[scripts/aerich_helper.py](../scripts/aerich_helper.py) 是一个可以简化这个流程的辅助脚本。将 `--help` 传递给脚本以查看详细的使用指南。

### 增强

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ matplotlib==3.8.4
# db
asyncpg==0.29.0
tortoise-orm[accel]==0.20.0
aerich==0.6.3
aerich==0.7.2

# network
aiohttp[speedups]==3.9.5
Expand Down
181 changes: 181 additions & 0 deletions scripts/aerich_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
#!/usr/bin/env python3

from typing import Final, ClassVar, Callable, Awaitable

import aerich
import argparse
import importlib.util
import inspect
import logging
import sys
from enum import Enum
from pathlib import Path
from tortoise import run_async

SELF_DIR: Final[Path] = Path(__file__).parent
PROJECT_ROOT: Final[Path] = SELF_DIR.parent
CONFIG_DIR: Final[Path] = PROJECT_ROOT / 'config'
DB_PKG_DIR: Final[Path] = PROJECT_ROOT / 'src' / 'db'

# import only models.py, without importing the db package
_models_module_name: Final[str] = 'models'
_models_path: Final[Path] = DB_PKG_DIR / f'{_models_module_name}.py'
_module_spec = importlib.util.spec_from_file_location(_models_module_name, _models_path)
models = importlib.util.module_from_spec(_module_spec)
sys.modules[_models_module_name] = models
_module_spec.loader.exec_module(models)


def bool_helper(value: str) -> bool:
try:
return int(value) != 0
except ValueError:
return value.lower() not in {'', 'false', 'no', 'n'}


class DBType(Enum):
SQLITE = 'sqlite'
PGSQL = 'postgres'


class MigrationHelper:
DEFAULT_DB_URL: ClassVar[str] = f'{DBType.SQLITE.value}://{CONFIG_DIR.as_posix()}/db.sqlite3'
MIGRATION_DIR: ClassVar[dict[DBType, str]] = {
DBType.SQLITE: str(DB_PKG_DIR / 'migrations_sqlite'),
DBType.PGSQL: str(DB_PKG_DIR / 'migrations_pgsql')
}

commands_registered: Final[dict[str, Callable]] = dict(
upgrade=aerich.Command.upgrade,
downgrade=aerich.Command.downgrade,
heads=aerich.Command.heads,
history=aerich.Command.history,
# inspectdb=aerich.Command.inspectdb,
migrate=aerich.Command.migrate,
init_db=aerich.Command.init_db, # USE WITH CAUTION
)

@classmethod
def register_sub_parser(cls, arg_parser: argparse.ArgumentParser):
sub_parsers = arg_parser.add_subparsers(dest='command', required=True)
for command, func in cls.commands_registered.items():
command_arguments: list[dict] = []
for param in inspect.signature(func).parameters.values():
if param.name == 'self':
continue
param_type = param.annotation
param_type_helper = param_type
if param_type is bool:
param_type_helper = bool_helper
elif param_type not in {str, int, float}:
raise ValueError(f'Unsupported type: {param.annotation}')
if param.default is inspect.Parameter.empty:
command_arguments.append(dict(
name=param.name,
type=param_type_helper,
help=f'{param_type.__name__}',
))
else:
command_arguments.append(dict(
name=param.name,
type=param_type_helper,
default=param.default,
nargs='?',
help=f'{param_type.__name__}, default: {param.default}',
))
sub_parser = sub_parsers.add_parser(
command,
help=(
'Parameters: '
+ (
', '.join(f'{param["name"]} ({param["help"]})' for param in command_arguments)
if command_arguments
else '-'
)
)
)
for param in command_arguments:
param_name = param.pop('name')
sub_parser.add_argument(param_name, **param)

@staticmethod
def generate_tortoise_orm_config(db_url: str) -> dict:
return {
"connections": {"default": db_url},
"apps": {
"models": {
"models": ["aerich.models", models],
"default_connection": "default"
},
},
}

def __init__(self, db_url: str):
db_type = DBType(db_url.partition('://')[0])
self.aerich_cmd = aerich.Command(
tortoise_config=self.generate_tortoise_orm_config(db_url),
location=self.MIGRATION_DIR[db_type],
)
self.aerich_initialized = False

async def init(self):
if not self.aerich_initialized:
await self.aerich_cmd.init()
self.aerich_initialized = True

async def exec_command(self, command: str, *args, **kwargs):
if command not in self.commands_registered:
raise ValueError(f'Command {command} not registered.')
if not self.aerich_initialized:
await self.init()
logging.info('Aerich initialized')
logging.info(f'Executing command {command} with args: {args}, kwargs: {kwargs}')
maybe_coro = await self.commands_registered[command](self.aerich_cmd, *args, **kwargs)
if isinstance(maybe_coro, Awaitable):
result = await maybe_coro
else:
result = maybe_coro
logging.info(f'Command "{command}" executed with result:\n{result}')


def main():
parser = argparse.ArgumentParser(
description='Aerich helper script\n\n',
epilog=(
'To create a new migration:\n'
' 1. Create a new branch <foo> and make your changes there.\n'
' 2. Switch to the "dev" branch.\n'
' 3. Create a temporary database for the migration.\n'
' 4. Execute "aerich_helper.py --db-url <temp_db_url> upgrade True" to set up the temporary database.\n'
' 5. Switch back to the <foo> branch.\n'
' 6. Execute "aerich_helper.py --db-url <temp_db_url> migrate" to create the migration.\n'
' 7. Now you can safely delete the temporary database.\n'
),
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose logging',
)
parser.add_argument(
'--db-url', '-u',
type=str,
default=MigrationHelper.DEFAULT_DB_URL,
help=(
f'Database URL, default to {MigrationHelper.DEFAULT_DB_URL}\n'
f'Examples:\n'
f' {DBType.SQLITE.value}://path/to/db.sqlite3\n'
f' {DBType.PGSQL.value}://<user>:<password>@<host>:<port>/<dbname>\n'
),
)
MigrationHelper.register_sub_parser(parser)
args = parser.parse_args()
args_d = vars(args)
logging.basicConfig(level=logging.DEBUG if args_d.pop('verbose') else logging.INFO)
migration_helper = MigrationHelper(args_d.pop('db_url'))
run_async(migration_helper.exec_command(**args_d))


if __name__ == '__main__':
main()
33 changes: 33 additions & 0 deletions scripts/aerich_migrations_sql_to_py.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env python3

from typing import Final

from pathlib import Path

DB_PKG_DIR: Final[Path] = Path(__file__).parent.parent / 'src' / 'db'

MIGRATE_TEMPLATE: Final[str] = """from tortoise import BaseDBAsyncClient
async def upgrade(db: BaseDBAsyncClient) -> str:
return \"\"\"
{upgrade_sql}\"\"\"
async def downgrade(db: BaseDBAsyncClient) -> str:
return \"\"\"
{downgrade_sql}\"\"\"
"""

for migrations_dir in [DB_PKG_DIR / 'migrations_sqlite', DB_PKG_DIR / 'migrations_pgsql']:
for sql_migration in migrations_dir.glob('**/*.sql'):
sql = sql_migration.read_text()
upgrade_sql, _, downgrade_sql = sql.partition('-- upgrade --')[2].partition('-- downgrade --')
py_migration = sql_migration.with_suffix('.py')
py_migration.write_text(
MIGRATE_TEMPLATE.format(
upgrade_sql=upgrade_sql.strip(),
downgrade_sql=downgrade_sql.strip(),
),
newline='\n',
)
10 changes: 8 additions & 2 deletions setup.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@

replacePackagePath = partial(re.compile(r'^src').sub, 'rsstt')

source_packages = find_packages(include=['src', 'src.*'])
proj_packages = [replacePackagePath(name) for name in source_packages]
# DB migrations are not Python packages, but they should also be included in the package
source_packages = find_packages(PROJ_ROOT, include=['src', 'src.*'])
db_migrations_dirs = [
str(path.relative_to(PROJ_ROOT)).replace('/', '.')
for path in (PROJ_ROOT / 'src' / 'db').glob('migrations_*/**')
if path.is_dir()
]
proj_packages = [replacePackagePath(name) for name in source_packages + db_migrations_dirs]

setup(
version=version,
Expand Down
Loading

0 comments on commit b5525cc

Please sign in to comment.