From 8685c87e371a358b0e14be66ba6832be57e9f364 Mon Sep 17 00:00:00 2001 From: "itay.dar" Date: Wed, 24 Jul 2024 14:20:51 +0300 Subject: [PATCH 1/8] add echo as an extra param to allow disabling logs from db --- docs/async_orm.md | 25 +++++++++++++++---------- nest/core/database/orm_provider.py | 3 ++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/async_orm.md b/docs/async_orm.md index d5da712..63a570b 100644 --- a/docs/async_orm.md +++ b/docs/async_orm.md @@ -94,9 +94,11 @@ config = AsyncOrmProvider( ) ``` -Note: you can add any parameters that needed in order to configure the database connection. +Disable asyncpg logging by set the parameter of `echo=False` in the `AsyncOrmProvider` object. +more on engine parameters [here](https://docs.sqlalchemy.org/en/20/core/engines.html) `app_service.py` + ```python from nest.core import Injectable @@ -104,7 +106,7 @@ from nest.core import Injectable @Injectable class AppService: def __init__(self): - self.app_name = "MongoApp" + self.app_name = "AsyncOrmApp" self.app_version = "1.0.0" async def get_app_info(self): @@ -112,6 +114,7 @@ class AppService: ``` `app_controller.py` + ```python from nest.core import Controller, Get @@ -168,8 +171,10 @@ async def startup(): await config.create_all() ``` -`@Module(...)`: This is a decorator that defines a module. In PyNest, a module is a class annotated with a `@Module()` decorator. -The imports array includes the modules required by this module. In this case, ExampleModule is imported. The controllers and providers arrays are empty here, indicating this module doesn't directly provide any controllers or services. +`@Module(...)`: This is a decorator that defines a module. In PyNest, a module is a class annotated with a `@Module()` +decorator. +The imports array includes the modules required by this module. In this case, ExampleModule is imported. The controllers +and providers arrays are empty here, indicating this module doesn't directly provide any controllers or services. `PyNestFactory.create()` is a command to create an instance of the application. The AppModule is passed as an argument, which acts as the root module of the application. @@ -189,8 +194,6 @@ other parameters for efficient database access. AsyncSession from sqlalchemy.ext.asyncio is used for executing asynchronous database operations. It is essential for leveraging the full capabilities of SQLAlchemy 2.0 in an async environment. - - ## Implementing Async Features ### Creating Entities @@ -305,11 +308,12 @@ from sqlalchemy.ext.asyncio import AsyncSession @Controller("examples") class ExamplesController: - - def __init__(self, service: ExamplesService): + + def __init__(self, service: ExamplesService): self.service = service @Get("/") + async def get_examples(self, session: AsyncSession = Depends(config.get_db)): return await self.service.get_examples(session) @@ -330,11 +334,12 @@ from .examples_model import Examples @Controller("examples") class ExamplesController: - - def __init__(self, service: ExamplesService): + + def __init__(self, service: ExamplesService): self.service = service @Get("/") + async def get_examples(self): return await self.service.get_examples() diff --git a/nest/core/database/orm_provider.py b/nest/core/database/orm_provider.py index 6a2b40e..5fa8700 100644 --- a/nest/core/database/orm_provider.py +++ b/nest/core/database/orm_provider.py @@ -99,7 +99,8 @@ class AsyncOrmProvider(BaseOrmProvider): def __init__( self, db_type: str = "postgresql", config_params: dict = None, **kwargs ): - kwargs["engine_params"] = dict(echo=True) + echo = kwargs.get("echo", True) + kwargs["engine_params"] = dict(echo=echo) kwargs["session_params"] = dict(expire_on_commit=False, class_=AsyncSession) super().__init__( db_type=db_type, config_params=config_params, async_mode=True, **kwargs From 77b0dd5fa298adf1ba3617adb398815fe6490966 Mon Sep 17 00:00:00 2001 From: "itay.dar" Date: Fri, 6 Dec 2024 08:24:28 +0200 Subject: [PATCH 2/8] migrate pynest to work with poetry upgrade pynest dependencies (pydantic v2, fastapi v0.115.*, etc...) change pynest to support python 3.10 --- .github/workflows/cli_test.yaml | 36 +++-- .github/workflows/deploy_docs.yaml | 10 +- .github/workflows/integration_test.yaml | 22 +-- .github/workflows/tests.yaml | 32 +++-- nest/core/decorators/class_based_view.py | 18 ++- nest/plugins/__init__.py | 0 nest/plugins/controllers/__init__.py | 0 nest/plugins/modules/__init__.py | 0 nest/plugins/modules/auth/__init__.py | 0 nest/plugins/modules/redis/__init__.py | 4 - .../plugins/modules/redis/redis_controller.py | 26 ---- nest/plugins/modules/redis/redis_model.py | 14 -- nest/plugins/modules/redis/redis_module.py | 8 -- nest/plugins/modules/redis/redis_service.py | 30 ---- nest/plugins/services/__init__.py | 0 pyproject.toml | 134 ++++++++++-------- 16 files changed, 139 insertions(+), 195 deletions(-) delete mode 100644 nest/plugins/__init__.py delete mode 100644 nest/plugins/controllers/__init__.py delete mode 100644 nest/plugins/modules/__init__.py delete mode 100644 nest/plugins/modules/auth/__init__.py delete mode 100644 nest/plugins/modules/redis/__init__.py delete mode 100644 nest/plugins/modules/redis/redis_controller.py delete mode 100644 nest/plugins/modules/redis/redis_model.py delete mode 100644 nest/plugins/modules/redis/redis_module.py delete mode 100644 nest/plugins/modules/redis/redis_service.py delete mode 100644 nest/plugins/services/__init__.py diff --git a/.github/workflows/cli_test.yaml b/.github/workflows/cli_test.yaml index 7feef03..a28733f 100644 --- a/.github/workflows/cli_test.yaml +++ b/.github/workflows/cli_test.yaml @@ -1,6 +1,6 @@ name: CLI Test -on: [ push, workflow_call, pull_request ] +on: [push, workflow_call, pull_request] jobs: test: @@ -8,54 +8,54 @@ jobs: strategy: matrix: - app_type: [ "Blank", "SyncORM", "AsyncORM", "MongoDB", "PostgresSync", "PostgresAsync", "MySQLSync", "MySQLAsync" ] + app_type: ["Blank", "SyncORM", "AsyncORM", "MongoDB", "PostgresSync", "PostgresAsync", "MySQLSync", "MySQLAsync"] steps: - name: Check out repository code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: - python-version: '3.8' + python-version: '3.10' - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install . + pip install poetry + poetry install - name: Test CLI Commands run: | app_name="${{ matrix.app_type }}App" case "${{ matrix.app_type }}" in "Blank") - pynest generate application -n "$app_name" + poetry run pynest generate application -n "$app_name" ;; "SyncORM") - pynest generate application -n "$app_name" -db sqlite + poetry run pynest generate application -n "$app_name" -db sqlite ;; "AsyncORM") - pynest generate application -n "$app_name" -db sqlite --is-async + poetry run pynest generate application -n "$app_name" -db sqlite --is-async ;; "MongoDB") - pynest generate application -n "$app_name" -db mongodb + poetry run pynest generate application -n "$app_name" -db mongodb ;; "PostgresSync") - pynest generate application -n "$app_name" -db postgresql + poetry run pynest generate application -n "$app_name" -db postgresql ;; "PostgresAsync") - pynest generate application -n "$app_name" -db postgresql --is-async + poetry run pynest generate application -n "$app_name" -db postgresql --is-async ;; "MySQLSync") - pynest generate application -n "$app_name" -db mysql + poetry run pynest generate application -n "$app_name" -db mysql ;; "MySQLAsync") - pynest generate application -n "$app_name" -db mysql --is-async + poetry run pynest generate application -n "$app_name" -db mysql --is-async ;; esac cd "$app_name" - pynest generate resource -n user + poetry run pynest generate resource -n user - name: Verify Boilerplate run: | @@ -74,7 +74,6 @@ jobs: exit 1 fi - # List of expected files declare -a files=("main.py" "requirements.txt" "README.md") declare -a src_level_files=("app_module.py" "app_service.py" "app_controller.py") @@ -100,7 +99,6 @@ jobs: fi done - # Check each file in the list of module_files for file in "${module_files[@]}"; do if [ -f "$app_name/src/user/$file" ]; then @@ -111,4 +109,4 @@ jobs: fi done - echo "Boilerplate for ${{ matrix.app_type }} generated successfully." + echo "Boilerplate for ${{ matrix.app_type }} generated successfully." \ No newline at end of file diff --git a/.github/workflows/deploy_docs.yaml b/.github/workflows/deploy_docs.yaml index ffe9bdb..0c7822c 100644 --- a/.github/workflows/deploy_docs.yaml +++ b/.github/workflows/deploy_docs.yaml @@ -2,13 +2,11 @@ name: Deploy Docs on: [workflow_call, workflow_dispatch] -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write -# Allow one concurrent deployment concurrency: group: "pages" cancel-in-progress: true @@ -27,15 +25,15 @@ jobs: - name: Copy License File run: cp LICENSE docs/license.md - - name: Set up Python 3.10 + - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.10" - - name: Install MKDocs + - name: Install dependencies run: | - pip install --upgrade pip - pip install mkdocs-material mkdocstrings-python + pip install poetry + poetry install --with docs - name: Build docs run: mkdocs build --clean diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index c82f624..99aeeb0 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -12,17 +12,17 @@ jobs: steps: - name: Check out repository code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: - python-version: '3.8' + python-version: '3.10' - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install . + pip install poetry + poetry install - name: Start Application run: | @@ -38,15 +38,15 @@ jobs: fi if [ "${{ matrix.app_type }}" == "Blank" ]; then - pynest generate application -n "$app_name" + poetry run pynest generate application -n "$app_name" else - pynest generate application -n "$app_name" -db sqlite $is_async - pip install aiosqlite + poetry run pynest generate application -n "$app_name" -db sqlite $is_async + poetry add aiosqlite fi cd "$app_name" - pynest generate resource -n user - uvicorn "src.app_module:http_server" --host "0.0.0.0" --port 8000 --reload & + poetry run pynest generate resource -n user + poetry run uvicorn "src.app_module:http_server" --host "0.0.0.0" --port 8000 --reload & - name: Wait for the server to start run: sleep 10 @@ -62,4 +62,4 @@ jobs: curl -f http://localhost:8000/user/ - name: Kill the server - run: kill $(jobs -p) || true + run: kill $(jobs -p) || true \ No newline at end of file diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b9c3ae7..9b236dc 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -5,21 +5,23 @@ on: [push, workflow_call, pull_request] jobs: test: runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11"] + steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3 + + - name: Install Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} - - name: Install Python - id: setup_python - uses: actions/setup-python@v4 - with: - python-version: | - 3.7 - 3.8 - 3.9 - 3.10 - 3.11 + - name: Install dependencies + run: | + pip install poetry + poetry install --with test - - name: Run tests - run: | - pip install -r requirements-tests.txt - pytest tests \ No newline at end of file + - name: Run tests + run: | + poetry run pytest tests \ No newline at end of file diff --git a/nest/core/decorators/class_based_view.py b/nest/core/decorators/class_based_view.py index a465542..18cf0b8 100644 --- a/nest/core/decorators/class_based_view.py +++ b/nest/core/decorators/class_based_view.py @@ -4,16 +4,24 @@ """ import inspect -from typing import Any, Callable, List, Type, TypeVar, Union, get_type_hints +from typing import ( + Any, + Callable, + ClassVar, + List, + Type, + TypeVar, + Union, + get_origin, + get_type_hints, +) from fastapi import APIRouter, Depends -from pydantic.typing import is_classvar from starlette.routing import Route, WebSocketRoute T = TypeVar("T") K = TypeVar("K", bound=Callable[..., Any]) - CBV_CLASS_KEY = "__cbv_class__" @@ -61,7 +69,7 @@ def _init_cbv(cls: Type[Any]) -> None: ] dependency_names: List[str] = [] for name, hint in get_type_hints(cls).items(): - if is_classvar(hint): + if get_origin(hint) is ClassVar: continue parameter_kwargs = {"default": getattr(cls, name, Ellipsis)} dependency_names.append(name) @@ -102,4 +110,4 @@ def _update_cbv_route_endpoint_signature( for parameter in old_parameters[1:] ] new_signature = old_signature.replace(parameters=new_parameters) - setattr(route.endpoint, "__signature__", new_signature) + setattr(route.endpoint, "__signature__", new_signature) \ No newline at end of file diff --git a/nest/plugins/__init__.py b/nest/plugins/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nest/plugins/controllers/__init__.py b/nest/plugins/controllers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nest/plugins/modules/__init__.py b/nest/plugins/modules/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nest/plugins/modules/auth/__init__.py b/nest/plugins/modules/auth/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nest/plugins/modules/redis/__init__.py b/nest/plugins/modules/redis/__init__.py deleted file mode 100644 index 013a218..0000000 --- a/nest/plugins/modules/redis/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from nest.plugins.modules.redis.redis_controller import RedisController -from nest.plugins.modules.redis.redis_model import RedisInput -from nest.plugins.modules.redis.redis_module import RedisModule -from nest.plugins.modules.redis.redis_service import RedisService diff --git a/nest/plugins/modules/redis/redis_controller.py b/nest/plugins/modules/redis/redis_controller.py deleted file mode 100644 index c5c4954..0000000 --- a/nest/plugins/modules/redis/redis_controller.py +++ /dev/null @@ -1,26 +0,0 @@ -from nest.core import Controller, Delete, Depends, Get, Post -from nest.plugins.modules.redis.redis_model import RedisInput -from nest.plugins.modules.redis.redis_service import RedisService - - -@Controller("redis") -class RedisController: - - def __init__(self, redis_service: RedisService): - self.redis_service = redis_service - - @Get("/{key}") - def get(self, key: str): - return self.redis_service.get(key) - - @Post("/") - def set(self, redis_input: RedisInput): - return self.redis_service.set(redis_input) - - @Delete("/{key}") - def delete(self, key: str): - return self.redis_service.delete(key) - - @Get("/exists/{key}") - def exists(self, key: str): - return self.redis_service.exists(key) diff --git a/nest/plugins/modules/redis/redis_model.py b/nest/plugins/modules/redis/redis_model.py deleted file mode 100644 index d852228..0000000 --- a/nest/plugins/modules/redis/redis_model.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Any - -from pydantic import BaseModel, BaseSettings - - -class RedisInput(BaseModel): - key: str - value: Any - - -class RedisConfig(BaseSettings): - REDIS_HOST: str = "localhost" - REDIS_PORT: int = 6379 - REDIS_DB: int = 0 diff --git a/nest/plugins/modules/redis/redis_module.py b/nest/plugins/modules/redis/redis_module.py deleted file mode 100644 index 95ff6ab..0000000 --- a/nest/plugins/modules/redis/redis_module.py +++ /dev/null @@ -1,8 +0,0 @@ -from nest.core import Module -from nest.plugins.modules.redis.redis_controller import RedisController -from nest.plugins.modules.redis.redis_service import RedisService - - -@Module(controllers=[RedisController], providers=[RedisService], imports=[]) -class RedisModule: - pass diff --git a/nest/plugins/modules/redis/redis_service.py b/nest/plugins/modules/redis/redis_service.py deleted file mode 100644 index 8464873..0000000 --- a/nest/plugins/modules/redis/redis_service.py +++ /dev/null @@ -1,30 +0,0 @@ -import redis -from fastapi import HTTPException - -from nest.core import Injectable -from nest.plugins.modules.redis.redis_model import RedisConfig, RedisInput - - -@Injectable() -class RedisService: - def __init__(self): - self.redis_config = RedisConfig() - self.redis_client = redis.StrictRedis( - host=self.redis_config.REDIS_HOST, - port=self.redis_config.REDIS_PORT, - db=self.redis_config.REDIS_DB, - ) - - def set(self, redis_input: RedisInput): - if self.exists(redis_input.key): - raise HTTPException(status_code=400, detail="Key already exists") - self.redis_client.set(redis_input.key, redis_input.value) - - def get(self, redis_key: str): - return self.redis_client.get(redis_key) - - def exists(self, redis_key: str): - return self.redis_client.exists(redis_key) - - def delete(self, redis_key: str): - self.redis_client.delete(redis_key) diff --git a/nest/plugins/services/__init__.py b/nest/plugins/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pyproject.toml b/pyproject.toml index 9acd99e..edc143a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,48 +1,85 @@ [build-system] -requires = ["setuptools>=61.0", "wheel>=0.37.0"] -build-backend = "setuptools.build_meta" +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" -[project] +[tool.poetry] name = "pynest-api" +version = "0.3.1" description = "PyNest is a FastAPI Abstraction for building microservices, influenced by NestJS." +authors = ["itay.dar "] readme = "README.md" -requires-python = ">=3.8.1" -license = { file = "LICENSE" } -authors = [ - { name = "Itay Dar", email = "itay2803@gmail.com" }, +homepage = "https://github.com/PythonNest/PyNest" +documentation = "https://pythonnest.github.io/PyNest/" +packages = [ + { include = "nest" } ] -dynamic = ["version"] classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "License :: OSI Approved :: MIT License", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", ] -dependencies = [ - "click>=8.1.6", - "fastapi>=0.88.0,<1.0.0", - "python-dotenv>=1.0.0", - "uvicorn>=0.23.1", - "PyYAML>=6.0.1", - "astor>=0.8.1", - "black>=23.11.0", - "injector>=0.20.1", - "pydantic<2.0.0", - "sqlalchemy == 2.0.19", - "alembic == 1.7.5", -] -[tool.setuptools.dynamic] -version = { attr = "nest.__init__.__version__" } -[tool.pip] -index-url = "https://pypi.org/simple" -trusted-host = ["pypi.org", "files.pythonhosted.org"] -[tools.black] +[tool.poetry.dependencies] +python = "^3.8" +# Core dependencies +click = "^8.1.7" +injector = "^0.22.0" +astor = "^0.8.1" +pyyaml = "^6.0.2" +black = "^24.10.0" +fastapi = "^0.115.4" +pydantic = "^2.9.2" +uvicorn = "^0.32.0" + + +# Optional dependencies +sqlalchemy = { version = "^2.0.36", optional = true } +asyncpg = { version = "^0.30.0", optional = true } +psycopg2 = { version = "^2.9.3", optional = true } +alembic = { version = "^1.13.3", optional = true } +beanie = { version = "^1.27.0", optional = true } +python-dotenv = { version = "^1.0.1", optional = true } +greenlet = { version = "^3.1.1", optional = true } + + + +[tool.poetry.extras] +postgres = ["sqlalchemy", "asyncpg", "psycopg2", "alembic", "greenlet", "python-dotenv"] +mongo = ["beanie", "python-dotenv"] +test = ["pytest"] + +[tool.poetry.group.build.dependencies] +setuptools = "^75.3.0" +wheel = "^0.44.0" +build = "^1.2.2.post1" +twine = "^5.1.1" +git-changelog = "^2.5.2" + +[tool.poetry.group.test.dependencies] +pytest = "^7.0.1" +fastapi = "^0.115.4" +sqlalchemy = "^2.0.36" +motor = "^3.2.0" +beanie = "^1.27.0" +pydantic = "^2.9.2" +python-dotenv = "^1.0.1" +uvicorn = "^0.32.0" + +[tool.poetry.group.docs.dependencies] +mkdocs-material = "^9.5.43" +mkdocstrings-python = "^1.12.2" + + +[tool.black] force-exclude = ''' /( | /*venv* @@ -58,34 +95,17 @@ force-exclude = ''' )/ ''' -[project.optional-dependencies] -test = [ - "pytest == 6.2.5", -] - -orm = [ - "sqlalchemy == 2.0.19", - "alembic == 1.7.4", -] -mongo = [ - "pymongo == 3.12.0", - "motor == 3.2.0", - "beanie == 1.20.0", -] - -[project.scripts] -pynest = "nest.cli.cli:nest_cli" - -[tool.setuptools.packages.find] -include = ["nest*"] -namespaces = false - [tool.mypy] exclude = [ "/*venv*" ] ignore_missing_imports = true -[project.urls] -"Homepage" = "https://github.com/PythonNest/PyNest" -"Documentation" = "https://pythonnest.github.io/PyNest/" +[tool.poetry.urls] +Homepage = "https://github.com/PythonNest/PyNest" +Documentation = "https://pythonnest.github.io/PyNest/" + +[tool.poetry.scripts] +pynest = "nest.cli.cli:nest_cli" + + From 7985e844beee41c66165f79dbb3e0a807cfd55d3 Mon Sep 17 00:00:00 2001 From: "itay.dar" Date: Fri, 6 Dec 2024 08:26:07 +0200 Subject: [PATCH 3/8] make workflow support in python 3.8 --- .github/workflows/cli_test.yaml | 2 +- .github/workflows/deploy_docs.yaml | 2 +- .github/workflows/integration_test.yaml | 2 +- .github/workflows/tests.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cli_test.yaml b/.github/workflows/cli_test.yaml index a28733f..9bba210 100644 --- a/.github/workflows/cli_test.yaml +++ b/.github/workflows/cli_test.yaml @@ -17,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.8' - name: Install dependencies run: | diff --git a/.github/workflows/deploy_docs.yaml b/.github/workflows/deploy_docs.yaml index 0c7822c..a084711 100644 --- a/.github/workflows/deploy_docs.yaml +++ b/.github/workflows/deploy_docs.yaml @@ -28,7 +28,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.8" - name: Install dependencies run: | diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 99aeeb0..1afd2d6 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -17,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.8' - name: Install dependencies run: | diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 9b236dc..c089c20 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 From 68bed4ec9d0903e2dcce6318ea62901236e1fb34 Mon Sep 17 00:00:00 2001 From: "itay.dar" Date: Fri, 6 Dec 2024 08:41:39 +0200 Subject: [PATCH 4/8] make workflow support in python 3.9 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index edc143a..59ca17d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,13 +29,12 @@ classifiers = [ [tool.poetry.dependencies] -python = "^3.8" +python = "^3.9" # Core dependencies click = "^8.1.7" injector = "^0.22.0" astor = "^0.8.1" pyyaml = "^6.0.2" -black = "^24.10.0" fastapi = "^0.115.4" pydantic = "^2.9.2" uvicorn = "^0.32.0" @@ -49,6 +48,7 @@ alembic = { version = "^1.13.3", optional = true } beanie = { version = "^1.27.0", optional = true } python-dotenv = { version = "^1.0.1", optional = true } greenlet = { version = "^3.1.1", optional = true } +black = "^24.10.0" From cd45764bdbcb948a201f355eecefa289f7982678 Mon Sep 17 00:00:00 2001 From: "itay.dar" Date: Fri, 6 Dec 2024 08:44:10 +0200 Subject: [PATCH 5/8] make odm provider support in kwargs --- nest/core/database/odm_config.py | 41 ++++++++++++++++++++++++++---- nest/core/database/odm_provider.py | 21 +++++++-------- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/nest/core/database/odm_config.py b/nest/core/database/odm_config.py index 8dc13b2..ac1b0d9 100644 --- a/nest/core/database/odm_config.py +++ b/nest/core/database/odm_config.py @@ -1,4 +1,5 @@ from nest.core.database.base_config import BaseProvider, ConfigFactoryBase +from urllib.parse import urlencode class MongoDBConfig(BaseProvider): @@ -12,17 +13,21 @@ class MongoDBConfig(BaseProvider): password (str): The password for database authentication. port (int): The database port number. srv (bool): Whether to use the SRV connection string. + uri (str): Optional pre-built MongoDB URI. + **kwargs: Additional keyword arguments to include in the URI query parameters. """ def __init__( self, - host: str, - db_name: str, + host: str = None, + db_name: str = None, user: str = None, password: str = None, port: int = 27017, srv: bool = False, + uri: str = None, + **kwargs, ): """ Initializes the MongoDBConfig instance. @@ -34,22 +39,48 @@ def __init__( password (str): The password for database authentication. port (int): The database port number. srv (bool): Whether to use the SRV connection string. + uri (str): Optional pre-built MongoDB URI. + **kwargs: Additional keyword arguments to include in the URI query parameters. """ self.srv = srv + self.uri = uri + self.kwargs = kwargs or {} super().__init__(host, db_name, user, password, port) def get_engine_url(self) -> str: """ - Returns the engine URL for the ORM. + Returns the engine URL for the ODM. Returns: str: The engine URL. """ + if self.uri: + return self.uri + + # Build the base URI + protocol = "mongodb+srv" if self.srv else "mongodb" + credentials = "" if self.user and self.password: - return f"mongodb{'+srv' if self.srv else ''}://{self.user}:{self.password}@{self.host}:{self.port}" - return f"mongodb{'+srv' if self.srv else ''}://{self.host}:{self.port}" + credentials = f"{self.user}:{self.password}@" + elif self.user: + credentials = f"{self.user}@" + + host_port = self.host or "" + if not self.srv and self.port: + host_port = f"{host_port}:{self.port}" + + db_name = f"/{self.db_name}" if self.db_name else "" + + # Build the query string from kwargs + query = "" + if self.kwargs: + query = "?" + urlencode(self.kwargs) + + # Build the full URI + uri = f"{protocol}://{credentials}{host_port}{db_name}{query}" + return uri class ConfigFactory(ConfigFactoryBase): diff --git a/nest/core/database/odm_provider.py b/nest/core/database/odm_provider.py index ead8d06..8f9f8e5 100644 --- a/nest/core/database/odm_provider.py +++ b/nest/core/database/odm_provider.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Type from beanie import Document, init_beanie from motor.motor_asyncio import AsyncIOMotorClient @@ -14,7 +14,7 @@ class OdmProvider: db_type (str, optional): The type of database. Defaults to "mongodb". config_params (dict, optional): Configuration parameters specific to the chosen database type. Defaults to None. - document_models (beanie.Document): a list of beanie.Document instances + document_models (List[Type[Document]]): A list of beanie.Document subclasses. Attributes: config: The configuration factory for the chosen database type. @@ -26,34 +26,35 @@ def __init__( self, db_type="mongodb", config_params: dict = None, - document_models: List[Document] = None, + document_models: list[Type[Document]] = None, ): """ - Initializes the OrmService instance. + Initializes the OdmProvider instance. Args: db_type (str, optional): The type of database. Defaults to "mongodb". config_params (dict, optional): Configuration parameters specific to the chosen database type. Defaults to None. - document_models (beanie.Document): a list of beanie.Document instances + document_models (List[Type[Document]]): A list of beanie.Document subclasses. """ - self.config_object = ConfigFactory(db_type=db_type).get_config() self.config = self.config_object(**config_params) self.config_url = self.config.get_engine_url() - self.document_models = document_models + self.document_models = document_models or [] async def create_all(self): + """ + Initializes the Beanie ODM with the provided document models. + """ self.check_document_models() client = AsyncIOMotorClient(self.config_url) await init_beanie( - database=client[self.config.db_name], document_models=self.document_models + database=client.get_default_database(), document_models=self.document_models ) def check_document_models(self): """ - Checks that the document_models argument is a list of beanie.Document instances. - + Checks that the document_models argument is a list of beanie.Document subclasses. """ if not isinstance(self.document_models, list): raise Exception("document_models should be a list") From 04732957d8b03bde613879dae6afa7d80e4a32bd Mon Sep 17 00:00:00 2001 From: "itay.dar" Date: Fri, 6 Dec 2024 08:54:19 +0200 Subject: [PATCH 6/8] fix unit tests make workflows run python ^3.9 --- .github/workflows/cli_test.yaml | 2 +- .github/workflows/deploy_docs.yaml | 2 +- .github/workflows/integration_test.yaml | 2 +- .github/workflows/tests.yaml | 2 +- tests/test_core/test_database/test_odm.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cli_test.yaml b/.github/workflows/cli_test.yaml index 9bba210..f6fb914 100644 --- a/.github/workflows/cli_test.yaml +++ b/.github/workflows/cli_test.yaml @@ -17,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.8' + python-version: '3.9' - name: Install dependencies run: | diff --git a/.github/workflows/deploy_docs.yaml b/.github/workflows/deploy_docs.yaml index a084711..c9073cd 100644 --- a/.github/workflows/deploy_docs.yaml +++ b/.github/workflows/deploy_docs.yaml @@ -28,7 +28,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.8" + python-version: "3.9" - name: Install dependencies run: | diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 1afd2d6..2f122cc 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -17,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.8' + python-version: '3.9' - name: Install dependencies run: | diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index c089c20..2ac7ca9 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 diff --git a/tests/test_core/test_database/test_odm.py b/tests/test_core/test_database/test_odm.py index 7b2ffb2..ac51013 100644 --- a/tests/test_core/test_database/test_odm.py +++ b/tests/test_core/test_database/test_odm.py @@ -36,7 +36,7 @@ def test_odm_service_definition(odm_service): def test_odm_service_config_url(odm_service): config_url = odm_service.config_url - assert config_url == "mongodb://user:password@host:port" + assert config_url == "mongodb://user:password@host:port/db_name" def test_mongo_config_definition(mongodb_config): From e64c65e0b808ce19a3a0208005e54af5dcf947ee Mon Sep 17 00:00:00 2001 From: "itay.dar" Date: Fri, 20 Dec 2024 08:20:51 +0200 Subject: [PATCH 7/8] remove dependency in old version var upgrade release workflow to work with poetry remove old requirements files. --- .github/workflows/release.yaml | 67 ++++++++++++------------- nest/cli/templates/base_template.py | 1 - nest/cli/templates/blank_template.py | 2 +- nest/cli/templates/cli_templates.py | 2 +- nest/cli/templates/mongo_db_template.py | 9 +--- nest/cli/templates/mongo_template.py | 2 +- nest/cli/templates/mysql_template.py | 4 +- nest/cli/templates/postgres_template.py | 4 +- nest/cli/templates/sqlite_template.py | 4 +- requirements-dev.txt | 10 ---- requirements-release.txt | 19 ------- requirements-tests.txt | 12 ----- 12 files changed, 42 insertions(+), 94 deletions(-) delete mode 100644 requirements-dev.txt delete mode 100644 requirements-release.txt delete mode 100644 requirements-tests.txt diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7388708..4717296 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -17,7 +17,7 @@ on: - patch env: - VERSION_FILE_PATH: nest/__init__.py + VERSION_FILE_PATH: pyproject.toml CHANGELOG_FILE_PATH: CHANGELOG.md jobs: @@ -36,65 +36,60 @@ jobs: with: token: ${{ secrets.RELEASE_GIT_TOKEN }} - - name: Set version + - name: Install Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install Poetry run: | - VERSION_REGEX='^(__version__ = \")(([[:digit:]]+\.)*[[:digit:]]+)((a|b|rc)[[:digit:]]+)?(\.post[[:digit:]]+)?(.dev[[:digit:]]+)?(\")$' - - # get current version from version file - CURRENT_VERSION=`sed -n -E "s/$VERSION_REGEX/\2/p" $VERSION_FILE_PATH` + pip install poetry + + - name: Set version using Poetry + run: | + # Extract the current version + CURRENT_VERSION=$(poetry version -s) echo "Current version: $CURRENT_VERSION" - # switch case for incrementing_version based on input + # Increment version case ${{ inputs.increment_version }} in major) - NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{$1=$1+1; $2=0; $3=0; print $0}' OFS=".") - ;; + NEW_VERSION=$(poetry version major | awk '{print $NF}') + ;; minor) - NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{$2=$2+1; $3=0; print $0}' OFS=".") - ;; + NEW_VERSION=$(poetry version minor | awk '{print $NF}') + ;; patch) - NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{$3=$3+1; print $0}' OFS=".") - ;; + NEW_VERSION=$(poetry version patch | awk '{print $NF}') + ;; *) - echo "Invalid input for increment_version" - exit 1 - ;; + echo "Invalid input for increment_version" + exit 1 + ;; esac echo "New version: $NEW_VERSION" echo "RELEASE_VERSION=$NEW_VERSION" >> $GITHUB_ENV - - # update version file - sed -i -E "s/$VERSION_REGEX/\1$NEW_VERSION\8/" $VERSION_FILE_PATH - - - name: Install python - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - - name: Install python dependencies - run: | - pip install --upgrade pip - pip install -r requirements-release.txt - name: Update CHANGELOG.md run: git-changelog . -o $CHANGELOG_FILE_PATH - - name: Build package - run: python -m build + - name: Build package with Poetry + run: | + poetry build - name: Commit and push changes run: | git config --global user.name "github-actions" git config --global user.email "github@actions.com" - git add $CHANGELOG_FILE_PATH $VERSION_FILE_PATH + git add $CHANGELOG_FILE_PATH pyproject.toml git commit -m "Increment version to $NEW_VERSION" git push - - name: Publish a Python distribution to PyPI + - name: Publish package to PyPI run: | - python -m twine upload dist/* -u ${{ secrets.PYPI_API_USER }} -p ${{ secrets.PYPI_API_TOKEN }} + poetry publish --username ${{ secrets.PYPI_API_USER }} --password ${{ secrets.PYPI_API_TOKEN }} - - name: Release New Version + - name: Create GitHub release uses: softprops/action-gh-release@v1 with: name: v${{ env.RELEASE_VERSION }} @@ -106,4 +101,4 @@ jobs: contents: read pages: write id-token: write - uses: ./.github/workflows/deploy_docs.yaml \ No newline at end of file + uses: ./.github/workflows/deploy_docs.yaml diff --git a/nest/cli/templates/base_template.py b/nest/cli/templates/base_template.py index 7d490a3..b36a7ed 100644 --- a/nest/cli/templates/base_template.py +++ b/nest/cli/templates/base_template.py @@ -25,7 +25,6 @@ def __init__(self, module_name: str): ) self.class_name = f"{self.capitalized_module_name}Module" self.base_path = Path(os.getcwd()) - self.version = __version__ self.nest_path = Path(__file__).parent.parent.parent @staticmethod diff --git a/nest/cli/templates/blank_template.py b/nest/cli/templates/blank_template.py index 50b1fd2..063e53d 100644 --- a/nest/cli/templates/blank_template.py +++ b/nest/cli/templates/blank_template.py @@ -139,4 +139,4 @@ def settings_file(self): """ def requirements_file(self): - return f"""pynest-api=={self.version}""" + return f"""pynest-api""" diff --git a/nest/cli/templates/cli_templates.py b/nest/cli/templates/cli_templates.py index 2720fcc..1f83e9e 100644 --- a/nest/cli/templates/cli_templates.py +++ b/nest/cli/templates/cli_templates.py @@ -144,7 +144,7 @@ def generate_project(self, project_name: str): self.create_template(src_path / "app_service.py", self.app_service_file()) def requirements_file(self): - return f"""pynest-api=={self.version}""" + return f"""pynest-api""" def config_file(self): return "" diff --git a/nest/cli/templates/mongo_db_template.py b/nest/cli/templates/mongo_db_template.py index eb1338c..ac39e99 100644 --- a/nest/cli/templates/mongo_db_template.py +++ b/nest/cli/templates/mongo_db_template.py @@ -82,13 +82,8 @@ class Config: """ def generate_requirements_file(self) -> str: - return f"""click==8.1.6 -fastapi==0.95.1 -python-dotenv==1.0.0 -uvicorn==0.23.1 -motor==3.2.0 -beanie==1.20.0 -pynest-api=={self.version} + return f""" +pynest-api """ def generate_dockerfile(self) -> str: diff --git a/nest/cli/templates/mongo_template.py b/nest/cli/templates/mongo_template.py index c36bdab..5554398 100644 --- a/nest/cli/templates/mongo_template.py +++ b/nest/cli/templates/mongo_template.py @@ -33,7 +33,7 @@ def config_file(self): """ def requirements_file(self): - return f"""pynest-api=={self.version} + return f"""pynest-api beanie==1.20.0""" def docker_file(self): diff --git a/nest/cli/templates/mysql_template.py b/nest/cli/templates/mysql_template.py index 83a22f7..fc19afc 100644 --- a/nest/cli/templates/mysql_template.py +++ b/nest/cli/templates/mysql_template.py @@ -31,7 +31,7 @@ def config_file(self): """ def requirements_file(self): - return f"""pynest-api=={self.version} + return f"""pynest-api mysql-connector-python==8.2.0 """ @@ -63,6 +63,6 @@ def config_file(self): """ def requirements_file(self): - return f"""pynest-api=={self.version} + return f"""pynest-api aiomysql==0.2.0 """ diff --git a/nest/cli/templates/postgres_template.py b/nest/cli/templates/postgres_template.py index ced9957..191cc95 100644 --- a/nest/cli/templates/postgres_template.py +++ b/nest/cli/templates/postgres_template.py @@ -31,7 +31,7 @@ def config_file(self): """ def requirements_file(self): - return f"""pynest-api=={self.version} + return f"""pynest-api psycopg2==2.9.6 """ @@ -63,6 +63,6 @@ def config_file(self): """ def requirements_file(self): - return f"""pynest-api=={self.version} + return f"""pynest-api asyncpg==0.29.0 """ diff --git a/nest/cli/templates/sqlite_template.py b/nest/cli/templates/sqlite_template.py index 28e29fe..461a7d9 100644 --- a/nest/cli/templates/sqlite_template.py +++ b/nest/cli/templates/sqlite_template.py @@ -27,7 +27,7 @@ def config_file(self): """ def requirements_file(self): - return f"""pynest-api=={self.version}""" + return f"""pynest-api""" def docker_file(self): return """FROM tiangolo/uvicorn-gunicorn-fastapi:python3.11 @@ -60,7 +60,7 @@ def config_file(self): """ def requirements_file(self): - return f"""pynest-api=={self.version} + return f"""pynest-api aiosqlite==0.19.0""" def docker_file(self): diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index aa977bb..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,10 +0,0 @@ -fastapi==0.95.1 -black==21.12b0 -SQLAlchemy==2.0.19 -motor==3.2.0 -beanie==1.20.0 -click==8.1.6 -PyYAML==6.0.1 -injector==0.21.0 -pydantic<2.0.0 -astor>=0.8.1 \ No newline at end of file diff --git a/requirements-release.txt b/requirements-release.txt deleted file mode 100644 index 0d30d21..0000000 --- a/requirements-release.txt +++ /dev/null @@ -1,19 +0,0 @@ -fastapi==0.95.1 -black==21.12b0 -SQLAlchemy==2.0.19 -motor==3.2.0 -beanie==1.20.0 -PyYAML==6.0.1 -injector==0.21.0 -astor==0.8.1 - -# package release -setuptools -wheel -build -twine -git-changelog - -# docs release -mkdocstrings-python -mkdocs-material \ No newline at end of file diff --git a/requirements-tests.txt b/requirements-tests.txt deleted file mode 100644 index a17e03c..0000000 --- a/requirements-tests.txt +++ /dev/null @@ -1,12 +0,0 @@ -pytest>=7.0.1 -fastapi>=0.95.1 -black>=21.12b0 -SQLAlchemy>=2.0.19 -motor>=3.2.0 -beanie>=1.20.0 -PyYAML>=6.0.1 -injector>=0.21.0 -pydantic<2.0.0 -python-dotenv>=1.0.0 -uvicorn>=0.23.1 -astor>=0.8.1 From 403ca70bf8bab34b3895bcc7b07ed27b13d423b1 Mon Sep 17 00:00:00 2001 From: "itay.dar" Date: Fri, 20 Dec 2024 08:41:18 +0200 Subject: [PATCH 8/8] refactor templates - add tags to controller --- nest/cli/templates/abstract_empty_template.py | 2 +- nest/cli/templates/base_template.py | 2 +- nest/cli/templates/blank_template.py | 2 +- nest/cli/templates/mongo_template.py | 2 +- nest/cli/templates/orm_template.py | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nest/cli/templates/abstract_empty_template.py b/nest/cli/templates/abstract_empty_template.py index 702464e..dfeaa41 100644 --- a/nest/cli/templates/abstract_empty_template.py +++ b/nest/cli/templates/abstract_empty_template.py @@ -24,7 +24,7 @@ def generate_controller_file(self, name) -> str: from src.{name}.{name}_model import {self.capitalized_name} - @Controller("{name}") + @Controller("{name}", tag="{name}") class {self.capitalized_name}Controller: service: {self.capitalized_name}Service = Depends({self.capitalized_name}Service) diff --git a/nest/cli/templates/base_template.py b/nest/cli/templates/base_template.py index b36a7ed..36e412f 100644 --- a/nest/cli/templates/base_template.py +++ b/nest/cli/templates/base_template.py @@ -367,7 +367,7 @@ def generate_module(self, module_name: str, path: str = None): def generate_empty_controller_file(self) -> str: return f"""from nest.core import Controller -@Controller("{self.module_name}") +@Controller("{self.module_name}", tag="{self.module_name}") class {self.capitalized_module_name}Controller: ... """ diff --git a/nest/cli/templates/blank_template.py b/nest/cli/templates/blank_template.py index 063e53d..cde225c 100644 --- a/nest/cli/templates/blank_template.py +++ b/nest/cli/templates/blank_template.py @@ -78,7 +78,7 @@ def controller_file(self): from .{self.module_name}_model import {self.capitalized_module_name} -@Controller("{self.module_name}") +@Controller("{self.module_name}", tag="{self.module_name}") class {self.capitalized_module_name}Controller: def __init__(self, {self.module_name}_service: {self.capitalized_module_name}Service): diff --git a/nest/cli/templates/mongo_template.py b/nest/cli/templates/mongo_template.py index 5554398..6ef3605 100644 --- a/nest/cli/templates/mongo_template.py +++ b/nest/cli/templates/mongo_template.py @@ -61,7 +61,7 @@ def controller_file(self): from .{self.module_name}_model import {self.capitalized_module_name} -@Controller("{self.module_name}") +@Controller("{self.module_name}", tag="{self.module_name}") class {self.capitalized_module_name}Controller: def __init__(self, {self.module_name}_service: {self.capitalized_module_name}Service): diff --git a/nest/cli/templates/orm_template.py b/nest/cli/templates/orm_template.py index 5230e4a..67f8795 100644 --- a/nest/cli/templates/orm_template.py +++ b/nest/cli/templates/orm_template.py @@ -127,7 +127,7 @@ def controller_file(self): from .{self.module_name}_model import {self.capitalized_module_name} -@Controller("{self.module_name}") +@Controller("{self.module_name}", tag="{self.module_name}") class {self.capitalized_module_name}Controller: def __init__(self, {self.module_name}_service: {self.capitalized_module_name}Service): @@ -293,7 +293,7 @@ def controller_file(self): from .{self.module_name}_model import {self.capitalized_module_name} -@Controller("{self.module_name}") +@Controller("{self.module_name}", tag="{self.module_name}") class {self.capitalized_module_name}Controller: def __init__(self, {self.module_name}_service: {self.capitalized_module_name}Service):