Skip to content

Commit

Permalink
Merge pull request #10 from PythonNest/fix_004_version
Browse files Browse the repository at this point in the history
Fix 004 version
  • Loading branch information
ItayTheDar committed Aug 2, 2023
2 parents 249cbdb + 374cd53 commit ff62e4e
Show file tree
Hide file tree
Showing 16 changed files with 271 additions and 85 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ jobs:
- name: Run tests
run: |
pip install -r requirements_dev.txt
pip install -r requirements-dev.txt
pytest tests
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@
<em>PyNest is a Python framework built on top of FastAPI that follows the modular architecture of NestJS</em>
</p>
<p align="center">
<a href="https://pypi.org/project/pynest-api" target="_blank">
<img src="https://img.shields.io/pypi/v/pynest-api?color=%2334D058&label=pypi%20package" alt="Package version">
</a>
<a href="https://pypi.org/project/pynest-api" target="_blank">
<img src="https://img.shields.io/pypi/pyversions/pynest-api.svg?color=%2334D058" alt="Supported Python versions">
</a>
<a href="https://pypi.org/project/pynest-api">
<img src="https://img.shields.io/pypi/v/pynest-api?color=%2334D058&label=pypi%20package" alt="Version">
</a>
<a href="https://pypi.org/project/pynest-api">
<img src="https://img.shields.io/pypi/pyversions/pynest-api.svg?color=%2334D058" alt="Python">
</a>
<a href="https://pepy.tech/project/pynest-api">
<img src="https://static.pepy.tech/personalized-badge/pynest-api?period=month&units=international_system&left_color=grey&right_color=brightgreen&left_text=Downloads" alt="Downloads">
</a>
<a href="https://github.com/PythonNest/PyNest/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/PythonNest/Pynest" alt="License">
</a>
</p>

# PyNest - Description

# Description

PyNest is designed to help structure your APIs in an intuitive, easy to understand, and enjoyable way.

Expand Down
36 changes: 33 additions & 3 deletions nest/cli/click_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,32 @@ def get_module_end_index(lines, modules_start_index):
return modules_end_index


def append_module_to_app(path_to_app_py: Path, new_module: str, db_type: str):
"""
Append a module import statement to the app.py file.
Args:
path_to_app_py (Path): The path to the app.py file.
new_module (str): The name of the new module to import.
db_type (str): The type of database to use.
return new_import, capitalized_new_module
Returns:
None
"""
split_new_module = new_module.split("_")
capitalized_new_module = "".join([word.capitalize() for word in split_new_module])

lines, _ = append_import(path_to_app_py, new_module, db_type)
# Find the line index where the modules list starts
modules_start_index = next(
(i for i, line in enumerate(lines) if "modules=[" in line),
len(lines) - 1, # If modules list not found, append the new module at the end
)
return modules_end_index


def append_module_to_app(path_to_app_py: Path, new_module: str, db_type: str):
"""
Append a module import statement to the app.py file.
Expand Down Expand Up @@ -408,6 +434,9 @@ def append_module_to_app(path_to_app_py: Path, new_module: str, db_type: str):
# Find the line index where the modules list ends
modules_end_index = get_module_end_index(lines, modules_start_index)

# Find the line index where the modules list ends
modules_end_index = get_module_end_index(lines, modules_start_index)

# Insert the new module before the closing bracket or at the end of the file
new_lines = (
lines[:modules_end_index]
Expand Down Expand Up @@ -467,18 +496,19 @@ def create_nest_module(name: str):
├── module_name_entity.py
├── module_name_module.py
"""
src_path = Path("/Users/itayd/PycharmProjects/testMongo") / "src"
src_path = Path(find_target_folder(os.getcwd(), "src"))

if name in [x.name for x in src_path.iterdir()]:
raise Exception(f"module {name} already exists")
# src_path = Path(find_target_folder(os.getcwd(), "src"))
if not src_path:
raise Exception("src folder not found")

config_file = src_path.parent / "orm_config.py"
if not config_file.exists():
raise Exception("orm_config.py file not found")
db_type = get_db_type(config_file)
add_document_to_odm_config(config_file, name, db_type)
if db_type == "mongodb":
add_document_to_odm_config(config_file, name, db_type)
module_path = src_path / name
create_folder(module_path)
create_file(module_path / "__init__.py", "")
Expand Down
Empty file removed nest/common/common.py
Empty file.
2 changes: 1 addition & 1 deletion nest/common/templates/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ def generate_app(db_type: str):
@app.on_event("startup")
async def startup():
{'await config.create_all()' if db_type == 'mongodb' else 'config.create_all()'}
"""
"""
2 changes: 2 additions & 0 deletions nest/common/templates/orm_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ def generate_orm_config(db_type: str):
config_params={{
"db_name": os.getenv("DB_NAME"),
"host": os.getenv("DB_HOST"),
"user": os.getenv("DB_USER"),
"password": os.getenv("DB_PASSWORD"),
"port": os.getenv("DB_PORT"),
}},
document_models=[Examples]
Expand Down
14 changes: 5 additions & 9 deletions nest/core/database/base_odm.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ class OdmService:
document_models (beanie.Document): a list of beanie.Document instances
Attributes:
Base: The declarative base class for defining ORM models.
config: The configuration factory for the chosen database type.
config_url: The URL generated from the database configuration parameters.
client: The Motor client for database connection.
"""

Expand Down Expand Up @@ -53,13 +51,11 @@ def check_document_models(self):
"""
Checks that the document_models argument is a list of beanie.Document instances.
Args:
document_models (beanie.Document): a list of beanie.Document instances
"""
if not isinstance(self.document_models, list):
raise Exception("document_models should be a list")
# for document_model in self.document_models:
# if not isinstance(document_model, Document):
# raise Exception(
# "Each item in document_models should be an instance of beanie.Document"
# )
for document_model in self.document_models:
if not issubclass(document_model, Document):
raise Exception(
"Each item in document_models should be a subclass of beanie.Document"
)
17 changes: 8 additions & 9 deletions nest/core/database/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def get_config(self):
assert self.db_type, "db_type is required"


class BaseOdmConfig:
class BaseConfig:
"""
Base abstract class for ODM (Object-Document Mapping) configurations.
Expand All @@ -52,29 +52,28 @@ def get_engine_url(self) -> str:
pass


class BaseProvider(BaseOdmConfig):
class BaseProvider(BaseConfig):
"""
Base class for ODM providers that implement the BaseOdmConfig interface.
Args:
host (str): The database host.
db_name (str): The name of the database.
port (int): The database port number.
Base class for Objets Mapping providers that implement the BaseConfig interface.
"""

def __init__(self, host: str, db_name: str, port: int):
def __init__(self, host: str, db_name: str, user: str, password: str, port: int):
"""
Initializes the BaseOdmProvider instance.
Args:
host (str): The database host.
db_name (str): The name of the database.
port (int): The database port number.
user (str): The username for database authentication.
password (str): The password for database authentication.
"""
self.host = host
self.db_name = db_name
self.user = user
self.password = password
self.port = port

def get_engine_url(self) -> str:
Expand Down
16 changes: 12 additions & 4 deletions nest/core/database/odm_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from nest.core.database.config import BaseOdmConfig, ConfigFactoryBase, BaseProvider
from nest.core.database.config import ConfigFactoryBase, BaseProvider


class MongoDBConfig(BaseProvider):
Expand All @@ -15,7 +15,15 @@ class MongoDBConfig(BaseProvider):
"""

def __init__(self, host: str, db_name: str, port: int = 27017, srv: bool = False):
def __init__(
self,
host: str,
db_name: str,
user: str = None,
password: str = None,
port: int = 27017,
srv: bool = False
):
"""
Initializes the MongoDBConfig instance.
Expand All @@ -29,7 +37,7 @@ def __init__(self, host: str, db_name: str, port: int = 27017, srv: bool = False
"""
self.srv = srv
super().__init__(host, db_name, port)
super().__init__(host, db_name, user, password, port)

def get_engine_url(self) -> str:
"""
Expand All @@ -39,7 +47,7 @@ def get_engine_url(self) -> str:
str: The engine URL.
"""
return f"mongodb{'+srv' if self.srv else ''}://{self.host}:{self.port}"
return f"mongodb{'+srv' if self.srv else ''}://{self.user}:{self.password}@{self.host}:{self.port}"


class ConfigFactory(ConfigFactoryBase):
Expand Down
2 changes: 1 addition & 1 deletion nest/core/database/orm_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def get_engine_url(self) -> str:
str: The engine URL.
"""
return f"mysql+mysqlconnector://{self.user}:{self.password}@{self.host}"
return f"mysql+mysqlconnector://{self.user}:{self.password}@{self.host}:{self.port}/{self.db_name}"


class SQLiteConfig(BaseConfig):
Expand Down
12 changes: 6 additions & 6 deletions nest/core/decorators/controller.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from fastapi_utils.cbv import _cbv as ClassBasedView
from fastapi_utils.inferring_router import InferringRouter
from fastapi.routing import APIRouter
from nest.core.decorators.helpers import class_based_view as ClassBasedView


def Controller(tag: str = None, prefix: str = None):
Expand All @@ -20,8 +20,8 @@ def Controller(tag: str = None, prefix: str = None):
if prefix.endswith("/"):
prefix = prefix[:-1]

def wrapper(cls):
router = InferringRouter(tags=[tag] if tag else None)
def wrapper(cls) -> ClassBasedView:
router = APIRouter(tags=[tag] if tag else None)

for name, method in cls.__dict__.items():
if callable(method) and hasattr(method, "method"):
Expand Down Expand Up @@ -70,10 +70,10 @@ def wrapper(cls):
else:
raise Exception("Invalid method")

def get_router():
def get_router() -> APIRouter:
"""
Returns:
InferringRouter: The router associated with the controller.
APIRouter: The router associated with the controller.
"""
return router

Expand Down
90 changes: 90 additions & 0 deletions nest/core/decorators/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'''
Credit: FastAPI-Utils
Source: https://github.com/dmontagu/fastapi-utils/blob/master/fastapi_utils/cbv.py
'''


import inspect
from typing import Any, Callable, List, Type, TypeVar, Union, get_type_hints

from fastapi import APIRouter, Depends
from pydantic.typing import is_classvar
from starlette.routing import Route, WebSocketRoute

T = TypeVar("T")

CBV_CLASS_KEY = "__cbv_class__"


def class_based_view(router: APIRouter, cls: Type[T]) -> Type[T]:
"""
Replaces any methods of the provided class `cls` that are endpoints of routes in `router` with updated
function calls that will properly inject an instance of `cls`.
"""
_init_cbv(cls)
cbv_router = APIRouter()
function_members = inspect.getmembers(cls, inspect.isfunction)
functions_set = set(func for _, func in function_members)
cbv_routes = [
route
for route in router.routes
if isinstance(route, (Route, WebSocketRoute)) and route.endpoint in functions_set
]
for route in cbv_routes:
router.routes.remove(route)
_update_cbv_route_endpoint_signature(cls, route)
cbv_router.routes.append(route)
router.include_router(cbv_router)
return cls


def _init_cbv(cls: Type[Any]) -> None:
"""
Idempotently modifies the provided `cls`, performing the following modifications:
* The `__init__` function is updated to set any class-annotated dependencies as instance attributes
* The `__signature__` attribute is updated to indicate to FastAPI what arguments should be passed to the initializer
"""
if getattr(cls, CBV_CLASS_KEY, False): # pragma: no cover
return # Already initialized
old_init: Callable[..., Any] = cls.__init__
old_signature = inspect.signature(old_init)
old_parameters = list(old_signature.parameters.values())[1:] # drop `self` parameter
new_parameters = [
x for x in old_parameters if x.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
]
dependency_names: List[str] = []
for name, hint in get_type_hints(cls).items():
if is_classvar(hint):
continue
parameter_kwargs = {"default": getattr(cls, name, Ellipsis)}
dependency_names.append(name)
new_parameters.append(
inspect.Parameter(name=name, kind=inspect.Parameter.KEYWORD_ONLY, annotation=hint, **parameter_kwargs)
)
new_signature = old_signature.replace(parameters=new_parameters)

def new_init(self: Any, *args: Any, **kwargs: Any) -> None:
for dep_name in dependency_names:
dep_value = kwargs.pop(dep_name)
setattr(self, dep_name, dep_value)
old_init(self, *args, **kwargs)

setattr(cls, "__signature__", new_signature)
setattr(cls, "__init__", new_init)
setattr(cls, CBV_CLASS_KEY, True)


def _update_cbv_route_endpoint_signature(cls: Type[Any], route: Union[Route, WebSocketRoute]) -> None:
"""
Fixes the endpoint signature for a cbv route to ensure FastAPI performs dependency injection properly.
"""
old_endpoint = route.endpoint
old_signature = inspect.signature(old_endpoint)
old_parameters: List[inspect.Parameter] = list(old_signature.parameters.values())
old_first_parameter = old_parameters[0]
new_first_parameter = old_first_parameter.replace(default=Depends(cls))
new_parameters = [new_first_parameter] + [
parameter.replace(kind=inspect.Parameter.KEYWORD_ONLY) for parameter in old_parameters[1:]
]
new_signature = old_signature.replace(parameters=new_parameters)
setattr(route.endpoint, "__signature__", new_signature)
16 changes: 3 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,16 @@ classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
]
dependencies = [
"anyio==3.6.2",
"click==8.1.3",
"click==8.1.6",
"fastapi==0.95.1",
"fastapi-utils==0.2.1",
"greenlet==2.0.2",
"h11==0.14.0",
"idna==3.4",
"pydantic==1.10.7",
"python-dotenv==1.0.0",
"sniffio==1.3.0",
"SQLAlchemy==1.4.48",
"starlette==0.26.1",
"typing_extensions==4.5.0",
"uvicorn==0.22.0",
"SQLAlchemy==2.0.19",
"uvicorn==0.23.1",
]

[tool.setuptools.dynamic]
Expand Down
File renamed without changes.
Loading

0 comments on commit ff62e4e

Please sign in to comment.