diff --git a/.github/workflows/cli_test.yaml b/.github/workflows/cli_test.yaml index 7750ad1..7feef03 100644 --- a/.github/workflows/cli_test.yaml +++ b/.github/workflows/cli_test.yaml @@ -29,33 +29,33 @@ jobs: app_name="${{ matrix.app_type }}App" case "${{ matrix.app_type }}" in "Blank") - pynest create-nest-app -n "$app_name" + pynest generate application -n "$app_name" ;; "SyncORM") - pynest create-nest-app -n "$app_name" -db sqlite + pynest generate application -n "$app_name" -db sqlite ;; "AsyncORM") - pynest create-nest-app -n "$app_name" -db sqlite --is-async + pynest generate application -n "$app_name" -db sqlite --is-async ;; "MongoDB") - pynest create-nest-app -n "$app_name" -db mongodb + pynest generate application -n "$app_name" -db mongodb ;; "PostgresSync") - pynest create-nest-app -n "$app_name" -db postgresql + pynest generate application -n "$app_name" -db postgresql ;; "PostgresAsync") - pynest create-nest-app -n "$app_name" -db postgresql --is-async + pynest generate application -n "$app_name" -db postgresql --is-async ;; "MySQLSync") - pynest create-nest-app -n "$app_name" -db mysql + pynest generate application -n "$app_name" -db mysql ;; "MySQLAsync") - pynest create-nest-app -n "$app_name" -db mysql --is-async + pynest generate application -n "$app_name" -db mysql --is-async ;; esac cd "$app_name" - pynest g module -n user + pynest generate resource -n user - name: Verify Boilerplate run: | diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 721f713..c82f624 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -38,14 +38,14 @@ jobs: fi if [ "${{ matrix.app_type }}" == "Blank" ]; then - pynest create-nest-app -n "$app_name" + pynest generate application -n "$app_name" else - pynest create-nest-app -n "$app_name" -db sqlite $is_async + pynest generate application -n "$app_name" -db sqlite $is_async pip install aiosqlite fi cd "$app_name" - pynest g module -n user + pynest generate resource -n user uvicorn "src.app_module:http_server" --host "0.0.0.0" --port 8000 --reload & - name: Wait for the server to start diff --git a/README.md b/README.md index c5c2293..985ac6d 100644 --- a/README.md +++ b/README.md @@ -19,16 +19,19 @@

- # Description PyNest is designed to help structure your APIs in an intuitive, easy to understand, and enjoyable way. -With PyNest, you can build scalable and maintainable APIs with ease. The framework supports dependency injection, type annotations, decorators, and code generation, making it easy to write clean and testable code. +With PyNest, you can build scalable and maintainable APIs with ease. The framework supports dependency injection, type +annotations, decorators, and code generation, making it easy to write clean and testable code. -This framework is not a direct port of NestJS to Python but rather a re-imagining of the framework specifically for Python developers, including backend engineers and ML engineers. It aims to assist them in building better and faster APIs for their data applications. +This framework is not a direct port of NestJS to Python but rather a re-imagining of the framework specifically for +Python developers, including backend engineers and ML engineers. It aims to assist them in building better and faster +APIs for their data applications. ## Getting Started + To get started with PyNest, you'll need to install it using pip: ```bash @@ -36,8 +39,9 @@ pip install pynest-api ``` ### Start with cli + ```bash -pynest create-nest-app -n my_app_name +pynest generate appplication -n my_app_name ``` this command will create a new project with the following structure: @@ -66,15 +70,16 @@ uvicorn "app:app" --host "0.0.0.0" --port "8000" --reload Now you can visit [OpenAPI](http://localhost:8000/docs) in your browser to see the default API documentation. -### Adding modules +### Adding resources To add a new module to your application, you can use the pynest generate module command: ```bash -pynest g module -n users +pynest generate resource -n users ``` -This will create a new module called ```users``` in your application with the following structure under the ```src``` folder: +This will create a new resource called ```users``` in your application with the following structure under the ```src``` +folder: ```text β”œβ”€β”€ users @@ -94,72 +99,78 @@ For more information on how to use PyNest, check out the official documentation ## PyNest CLI Usage Guide -This document provides a guide on how to use the PyNest Command Line Interface (CLI). Below are the available commands and their descriptions: +This document provides a guide on how to use the PyNest Command Line Interface (CLI). Below are the available commands +and their descriptions: ### `pynest` Command - **Description**: The main command group for PyNest CLI. -#### `create-nest-app` Subcommand +#### `generate application` Subcommand - **Description**: Create a new nest app. - **Options**: - - `--app-name`/`-n`: The name of the nest app (required). - - `--db-type`/`-db`: The type of the database (optional). You can specify PostgreSQL, MySQL, SQLite, or MongoDB. - - `--is-async`: Whether the project should be asynchronous (optional, default is False). + - `--app-name`/`-n`: The name of the nest app (required). + - `--db-type`/`-db`: The type of the database (optional). You can specify PostgreSQL, MySQL, SQLite, or MongoDB. + - `--is-async`: Whether the project should be asynchronous (optional, default is False). -### `g` command group +### `generate` command group - **Description**: Group command for generating boilerplate code. -#### `module` Subcommand +#### `resource` Subcommand - **Description**: Generate a new module (controller, service, entity, model, module). - **Options**: - - `--name`/`-n`: The name of the module (required). - + - `--name`/`-n`: The name of the module (required). #### CLI Examples -* create a blank nest application - -`pynest create-nest-app -n my_app_name` -* create a nest application with postgres database and async connection - -`pynest create-nest-app -n my_app_name -db postgresql --is-async` +* create a blank nest application - + `pynest generate application -n my_app_name` -* create new module - -`pynest g module -n users` +* create a nest application with postgres database and async connection - + `pynest generate application -n my_app_name -db postgresql --is-async` +* create new module - + `pynest generate resource -n users` ## Key Features + ### Modular Architecture -PyNest follows the modular architecture of NestJS, which allows for easy separation of concerns and code organization. Each module contains a collection of related controllers, services, and providers. +PyNest follows the modular architecture of NestJS, which allows for easy separation of concerns and code organization. +Each module contains a collection of related controllers, services, and providers. ### Dependency Injection -PyNest supports dependency injection, which makes it easy to manage dependencies and write testable code. You can easily inject services and providers into your controllers using decorators. +PyNest supports dependency injection, which makes it easy to manage dependencies and write testable code. You can easily +inject services and providers into your controllers using decorators. ### Decorators -PyNest makes extensive use of decorators to define routes, middleware, and other application components. This helps keep the code concise and easy to read. +PyNest makes extensive use of decorators to define routes, middleware, and other application components. This helps keep +the code concise and easy to read. ### Type Annotations -PyNest leverages Python's type annotations to provide better tooling and help prevent errors. You can annotate your controllers, services, and providers with types to make your code more robust. +PyNest leverages Python's type annotations to provide better tooling and help prevent errors. You can annotate your +controllers, services, and providers with types to make your code more robust. ### Code Generation -PyNest includes a code generation tool that can create boilerplate code for modules, controllers, and other components. This saves you time and helps you focus on writing the code that matters. +PyNest includes a code generation tool that can create boilerplate code for modules, controllers, and other components. +This saves you time and helps you focus on writing the code that matters. ## Future Plans -- [ ] Create plugins Marketplace for modules where developers can share their modules and download modules created by others. +- [ ] Create plugins Marketplace for modules where developers can share their modules and download modules created by + others. - [ ] Implement IOC mechanism and introduce Module decorator - [ ] Add support for new databases - [ ] Create out-of-the-box authentication module that can be easily integrated into any application. - [ ] Add support for other testing frameworks and create testing templates. -- [ ] Add support for other web frameworks (Flask, Django, etc.) - Same Architecture, different engine. - +- [ ] Add support for other web frameworks (Litestar, blackship, etc.) - Same Architecture, different engine. ## Contributing diff --git a/docs/async_orm.md b/docs/async_orm.md index 63a570b..490be6f 100644 --- a/docs/async_orm.md +++ b/docs/async_orm.md @@ -155,13 +155,8 @@ class AppModule: pass -app = PyNestFactory.create( - AppModule, - description="This is my FastAPI app drive by Async ORM Engine", - title="My App", - version="1.0.0", - debug=True, -) +app = PyNestFactory.create(AppModule, description="This is my FastAPI app drive by Async ORM Engine", title="My App", + version="1.0.0", debug=True) http_server = app.get_server() @@ -383,4 +378,4 @@ uvicorn "src.app_module:http_server" --host "0.0.0.0" --port "8000" --reload Application Example With MongoDB → - \ No newline at end of file + diff --git a/docs/blank.md b/docs/blank.md index 4c94677..c6ea9aa 100644 --- a/docs/blank.md +++ b/docs/blank.md @@ -102,6 +102,7 @@ from .app_controller import AppController from .app_service import AppService from fastapi import FastAPI + @Module( controllers=[AppController], providers=[AppService], @@ -111,13 +112,7 @@ class AppModule: pass -app = PyNestFactory.create( - AppModule, - description="This is my FastAPI app", - title="My App", - version="1.0.0", - debug=True, -) +app = PyNestFactory.create(AppModule, description="This is my FastAPI app", title="My App", version="1.0.0", debug=True) http_server: FastAPI = app.get_server() ``` @@ -234,4 +229,4 @@ Now you can access the application at http://localhost:8000/docs and test the en Application Example With ORM → - \ No newline at end of file + diff --git a/docs/cli.md b/docs/cli.md index 458b0e6..0b3adce 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -13,21 +13,22 @@ pip install pynest-api ## CLI Commands and Usage πŸ› οΈ The PyNest CLI provides a variety of commands to help you create and manage your PyNest applications. Below are the detailed descriptions of the available commands and their usage. -### create-nest-app +### Generate New Application Create a new PyNest application. ```bash -pynest create-nest-app --app-name [--db-type ] [--is-async] +pynest generate application --app-name [--db-type ] [--is-async] ``` **Options** * `--app-name`, `-n`: The name of the new application. (Required) * `--db-type`, `-db`: The type of database to use (postgresql, mysql, sqlite, mongodb). (Optional) * `--is-async`: Whether the project should be async or not. This is applicable only for relational databases. (Optional) +* `--is-cli`: Whether the project should be a CLI App. (Optional) #### Example ```bash -pynest create-nest-app --app-name my_pynest_app --db-type postgresql --is-async +pynest generate application --app-name my_pynest_app --db-type postgresql --is-async ``` This example will create a skeleton PyNest application named `my_pynest_app` with PostgreSQL as the database and async support. @@ -42,40 +43,100 @@ my_pynest_app/ β”‚ β”œβ”€β”€ app_module.py β”‚ β”œβ”€β”€ app_controller.py β”‚ β”œβ”€β”€ app_service.py +β”‚ β”œβ”€β”€ config.py β”œβ”€β”€ main.py β”œβ”€β”€ requirements.txt └── README.md ``` -Note: The actual file structure may vary based on the modules and components you create.: +!Note: The actual file structure may vary based on the modules and components you create -### generate (Alias: g) +### generate command Generate boilerplate code for various components of the application. #### Subcommands -**module** -Generate a new module with the associated controller, service, model and module files._ +**Resource** +Generate a new resource with the associated controller, service, model and module files, with respect to the project configurations (e.g. database type). ```bash -pynest g module --name +pynest generate resource --name ``` **Options** * `--name`, `-n`: The name of the new module. (Required) +* `--path`, `-p`: The path where the module should be created. (Optional) **Example** ```bash -pynest g module --name users +pynest generate resource --name users ``` -This will create a new module named `users` with the associated controller, service, model and module files. +This will create a new resource named `users` with the associated controller, service, model and module files. -Note: In the future the cli will support more subcommands like `controller`, `service`, `model` and `provider` and `resource` that will replace the current `g module` as in nestjs cli: +**Module** + +Generate a new module file, which can be used to group related components of the application. + +```bash +pynest generate module --name +``` + +**Options** + +* `--name`, `-n`: The name of the new module. (Required) +* `--path`, `-p`: The path where the module should be created. (Optional) + +**Example** +```bash +pynest generate module --name auth +``` + +This will create a new module named `auth` in the default path. + +**Controller** + +Generate a new controller file, which will handle the routing and responses for a specific resource. + +```bash +pynest generate controller --name +``` + +**Options** + +* `--name`, `-n`: The name of the new controller. (Required) +* `--path`, `-p`: The path where the controller should be created. (Optional) + +**Example** +```bash +pynest generate controller --name users +``` + +This will create a new controller named `users` in the default path. + +**Service** + +Generate a new service file, which will contain the business logic for a specific resource. + +```bash +pynest generate service --name +``` + +**Options** + +* `--name`, `-n`: The name of the new service. (Required) +* `--path`, `-p`: The path where the service should be created. (Optional) + +**Example** +```bash +pynest generate service --name users +``` + +This will create a new service named `users` in the default path. ## Best Practices 🌟 diff --git a/docs/controllers.md b/docs/controllers.md index ecc4569..d148eb9 100644 --- a/docs/controllers.md +++ b/docs/controllers.md @@ -26,15 +26,48 @@ class BookController: return {"message": "Book added successfully!"} ``` -In the example above: +Let's take this step by step: -The `@Controller('/books')` decorator defines a controller with the prefix `/books`. +```python +from nest.core import Controller, Get, Post +``` + +PyNest exposes an api for creating Controllers, a class that is responsible for the module routing and requests handling. + +```python +@Controller('/books') +``` + +The `@Controller` decorator is used to define a controller class. -The `BookController` class handles HTTP `GET` requests to `/books` and `POST` requests -to `/books` using the `@Get` and `@Post` decorators, -respectively. +The `/books` argument specifies the route path prefix for the controller, so all routes in the controller will be prefixed with `/books`. +```python +class BookController: + def __init__(self, book_service: BookService): + self.book_service = book_service +``` + +The book `BookController` class is defined with a constructor that takes a `BookService` dependency as an argument. The `BookService` dependency is injected into the BookController to handle the business logic. +PyNest ioc (Inversion of Control) +container will inject the `BookService` instance into the controller +when it is created so that with every invocation of the controller, +the same instance of the `BookService` is used to handle the requests. + +```python + @Get('/') + def get_books(self): + return self.book_service.get_books() + + @Post('/') + def add_book(self, book): + self.book_service.add_book(book) + return {"message": "Book added successfully!"} +``` + +The `@Get('/')` and `@Post('/')` decorators define routes for the controller. More on that in the [routing](#routing) section. + ## Routing @@ -43,20 +76,76 @@ Each controller can have multiple routes, and different routes can perform diffe ### Example ```python -from nest.core import Controller, Get +from nest.core import Controller, Get, Post + +@Controller('/book') +class BookController: + def __init__(self, book_service: BookService): + self.book_service = book_service -@Controller('/cats') -class CatsController: @Get('/') - def find_all(self): - return 'This action returns all cats' + def get_books(self): + return self.book_service.get_books() + + @Get('/:book_id') + def get_book(self, book_id: int): + return self.book_service.get_book(book_id) ``` In this example: -The `@Controller('/cats')` decorator specifies the route path prefix for the `CatsController`. -The `@Get('/')` decorator creates a handler for the HTTP `GET` requests to `/cats`. -When a `GET` request is made to `/cats`, the find_all method is invoked, returning a string response. +The `@Controller('/book')` decorator specifies the route path prefix for the `CatsController`. +The `@Get('/')` decorator creates a handler for the HTTP `GET` requests to `/book`. +When a `GET` request is made to `/book`, the find_all method is invoked, returning all the books in the database. +When a `GET` request is made to `/book/:book_id`, the find_by_id method is invoked, returning the book with the specified ID. + +## Http Methods + +Pynest support 5 http methodsβ€”Get, Post, Put, Delete, Patch. +Since pynest is an abstraction of fastapi, we can use those methods in the same way we use them in fastapi. + +### Example +```python +from nest.core import Controller, Get, Post + +from .book_service import BookService +from typing import List + + +@Controller('/book') +class BookController: + def __init__(self, book_service: BookService): + self.book_service = book_service + + @Get( + '/', + response_model=List[Book], + description="Get all books", + response_description="List of books" + ) + def get_books(self) -> List[Book]: + return self.book_service.get_books() +``` + +Let's take this step by step: + +```python + @Get( + '/', + response_model=List[Book], + description="Get all books", + response_description="List of books" + ) + def get_books(self) -> List[Book]: + return self.book_service.get_books() +``` + +The `@Get` decorator is used to define a route that handles HTTP GET requests. +The `/` argument specifies the route path for the get_books method. +The `response_model` argument specifies the response model for the route, which is a list of Book objects. +The `description` argument provides a description of the route, which is displayed in the API documentation (Swagger). +For more On that - [FastAPI Docs](https://fastapi.tiangolo.com/) + ## Creating Controllers Using the CLI (In Progress!) @@ -78,7 +167,7 @@ Controllers in PyNest handle HTTP requests and responses through various decorat that correspond to HTTP methods like GET, POST, PUT, DELETE, etc. -### Example +### Full CRUD Example ```python from nest.core import Controller, Get, Post, Put, Delete @@ -100,18 +189,15 @@ class BooksController: @Post('/') def add_book(self, book: Book): - self.book_service.add_book(book) - return {"message": "Book added successfully!"} + return self.book_service.add_book(book) @Put('/:book_id') def update_book(self, book_id: int , book: Book): - self.book_service.update_book(book_id, book) - return {"message": "Book updated successfully!"} + return self.book_service.update_book(book_id, book) @Delete('/:book_id') def delete_book(self, book_id: int): - self.book_service.delete_book(book_id) - return {"message": "Book deleted successfully!"} + return self.book_service.delete_book(book_id) ``` When we will go to the docs, we will see this api resource - diff --git a/docs/modules.md b/docs/modules.md index d397ef9..1dbe78a 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -34,11 +34,11 @@ from pynest.core import Module from .book_controller import BookController from .book_service import BookService -@Module({ - 'controllers': [BookController], - 'providers': [BookService], - 'exports': [BookService], -}) +@Module( + providers=[BookService], + controllers=[BookController], + exports=[BookService], +) class BookModule: pass ``` @@ -57,33 +57,17 @@ and the second module imports it. from pynest.core import Module from .book_module import BookModule -@Module({ - 'imports': [BookModule], -}) +@Module( + imports=[BookModule], +) class AppModule: pass ``` -In this example, the `AppModule` imports the `BookModule`, which means that all the providers exported by the `BookModule` are available in the `AppModule`. +In this example, the `AppModule` imports the `BookModule`, +which means that all the providers exported by the `BookModule` are available in the +`AppModule` and all the routes in `BookController` will be registered to the main application. -## Global Modules -Global modules provide a set of providers that should be available across the entire application without needing to import the module in every module's import array. - -```python -from pynest.core import Module -from .shared_service import SharedService - -@Global() -@Module({ - 'providers': [SharedService], - 'exports': [SharedService], - 'is_global': True, -}) -class SharedModule: - pass -``` - -Global modules should be registered only once, typically by the root or core module. This approach reduces boilerplate and makes the application more maintainable. --- diff --git a/docs/providers.md b/docs/providers.md index 3b077df..a6fb106 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -130,7 +130,7 @@ The EmailService is defined as a provider in the EmailModule and exported for us ## Conclusion -Providers are essential components in PyNest applications, handling business logic and other functionalities. +Providers are essential parts in PyNest applications, handling business logic and other functionalities. By defining, importing, exporting, and injecting providers, you can create modular and maintainable applications with clear separation of concerns. Understanding how providers work and how to use them effectively will help diff --git a/docs/sync_orm.md b/docs/sync_orm.md index 7ab75f1..b69b251 100644 --- a/docs/sync_orm.md +++ b/docs/sync_orm.md @@ -135,13 +135,8 @@ class AppModule: pass -app = PyNestFactory.create( - AppModule, - description="This is my FastAPI app drive by ORM Engine", - title="My App", - version="1.0.0", - debug=True, -) +app = PyNestFactory.create(AppModule, description="This is my FastAPI app drive by ORM Engine", title="My App", + version="1.0.0", debug=True) http_server: FastAPI = app.get_server() @@ -281,4 +276,5 @@ Now you can access the application at http://localhost:8000/docs and test the en Application Example With Async ORM → - \ No newline at end of file + + diff --git a/examples/CommandLineApp/__init__.py b/examples/CommandLineApp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/CommandLineApp/main.py b/examples/CommandLineApp/main.py new file mode 100644 index 0000000..a27e42c --- /dev/null +++ b/examples/CommandLineApp/main.py @@ -0,0 +1,4 @@ +from src.app_module import app + +if __name__ == "__main__": + app() diff --git a/examples/CommandLineApp/src/__init__.py b/examples/CommandLineApp/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/CommandLineApp/src/app_controller.py b/examples/CommandLineApp/src/app_controller.py new file mode 100644 index 0000000..f509a6c --- /dev/null +++ b/examples/CommandLineApp/src/app_controller.py @@ -0,0 +1,15 @@ +from nest.core.decorators.cli.cli_decorators import CliCommand, CliController + +from .app_service import AppService + + +@CliController("app") +class AppController: + + def __init__(self, app_service: AppService): + self.app_service = app_service + + @CliCommand("info") + def get_app_info(self): + app_info = self.app_service.get_app_info() + print(app_info) diff --git a/examples/CommandLineApp/src/app_module.py b/examples/CommandLineApp/src/app_module.py new file mode 100644 index 0000000..0c5b7fb --- /dev/null +++ b/examples/CommandLineApp/src/app_module.py @@ -0,0 +1,20 @@ +from nest.core import Module +from nest.core.cli_factory import CLIAppFactory +from nest.core.pynest_factory import PyNestFactory + +from .app_controller import AppController +from .app_service import AppService +from .user.user_module import UserModule +from .user.user_service import UserService + + +@Module( + imports=[UserModule], + controllers=[AppController], + providers=[UserService, AppService], +) +class AppModule: + pass + + +app = CLIAppFactory().create(AppModule) diff --git a/examples/CommandLineApp/src/app_service.py b/examples/CommandLineApp/src/app_service.py new file mode 100644 index 0000000..c98a121 --- /dev/null +++ b/examples/CommandLineApp/src/app_service.py @@ -0,0 +1,11 @@ +from nest.core import Injectable + + +@Injectable +class AppService: + def __init__(self): + self.app_name = "Pynest App" + self.app_version = "1.0.0" + + def get_app_info(self): + return {"app_name": self.app_name, "app_version": self.app_version} diff --git a/examples/CommandLineApp/src/user/__init__.py b/examples/CommandLineApp/src/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/CommandLineApp/src/user/user_controller.py b/examples/CommandLineApp/src/user/user_controller.py new file mode 100644 index 0000000..08d2547 --- /dev/null +++ b/examples/CommandLineApp/src/user/user_controller.py @@ -0,0 +1,28 @@ +import click + +from examples.CommandLineApp.src.user.user_service import UserService +from nest.core.decorators.cli.cli_decorators import CliCommand, CliController + + +class ListOptions: + USER = click.Option( + ["-u", "--user"], help="user name to retrieve", required=True, type=str + ) + + +@CliController("user") +class UserController: + + def __init__(self, user_service: UserService): + self.user_service = user_service + + @CliCommand("list", help="List all users") + def list_users(self): + return self.user_service.get_users() + + @CliCommand("show", help="Show user by id") + def show_user( + self, + user: ListOptions.USER, + ): + return self.user_service.get_user(user) diff --git a/examples/CommandLineApp/src/user/user_module.py b/examples/CommandLineApp/src/user/user_module.py new file mode 100644 index 0000000..b90a065 --- /dev/null +++ b/examples/CommandLineApp/src/user/user_module.py @@ -0,0 +1,9 @@ +from nest.core import Module + +from .user_controller import UserController +from .user_service import UserService + + +@Module(controllers=[UserController], providers=[UserService]) +class UserModule: + pass diff --git a/examples/CommandLineApp/src/user/user_service.py b/examples/CommandLineApp/src/user/user_service.py new file mode 100644 index 0000000..9af52df --- /dev/null +++ b/examples/CommandLineApp/src/user/user_service.py @@ -0,0 +1,11 @@ +from nest.core import Injectable + + +@Injectable +class UserService: + + def get_users(self): + print("This command show all users") + + def get_user(self, user_name: str): + print(f"This command show user with user_name: {user_name}") diff --git a/examples/OrmAsyncApp/src/product/product_entity.py b/examples/OrmAsyncApp/src/product/product_entity.py index 8a65844..b49480d 100644 --- a/examples/OrmAsyncApp/src/product/product_entity.py +++ b/examples/OrmAsyncApp/src/product/product_entity.py @@ -1,7 +1,8 @@ -from ..config import config from sqlalchemy import Integer, String from sqlalchemy.orm import Mapped, mapped_column +from ..config import config + class Product(config.Base): __tablename__ = "product" diff --git a/mkdocs.yml b/mkdocs.yml index 1ef0cee..f955e53 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -45,12 +45,13 @@ extra_css: - styles/extra.css nav: - - Introduction: introduction.md - - Getting Started: getting_started.md - - CLI Usage: cli.md - - Modules: modules.md - - Controllers: controllers.md - - Providers: providers.md + - Overview: + - Introduction: introduction.md + - Getting Started: getting_started.md + - CLI Usage: cli.md + - Modules: modules.md + - Controllers: controllers.md + - Providers: providers.md - Dependency Injection: dependency_injection.md - Deployment: - Docker: docker.md diff --git a/nest/cli/cli.py b/nest/cli/cli.py index 3d2150d..bbe16a1 100644 --- a/nest/cli/cli.py +++ b/nest/cli/cli.py @@ -1,73 +1,4 @@ -from typing import Optional +from nest.cli.src.app_module import nest_cli -import click - -from nest.cli.click_handlers import create_nest_app, create_nest_module - -### Options ### -APP_NAME = click.option( - "--app-name", # Changed the underscore to a hyphen for consistency - "-n", - help="The name of the nest app.", - required=True, - type=str, - default=".", -) - -DB_TYPE = click.option( - "--db-type", - "-db", - help="The type of the database (postgresql, mysql, sqlite, or mongo db).", - required=False, - default=None, - type=str, -) - -MODULE_NAME = click.option( - "--name", - "-n", - help="The name of the module.", - required=False, - type=str, -) - -IS_ASYNC = click.option( - "--is-async", # Changed the underscore to a hyphen for consistency - help="Whether the project should be async or not (only for relational databases).", - required=False, - is_flag=True, -) - - -@click.group() -def nest_cli() -> None: - pass - - -@nest_cli.command( - name="create-nest-app", - help="Create a new nest app.", -) -@APP_NAME -@DB_TYPE -@IS_ASYNC -def create_nest_app_command( - app_name: str = ".", db_type: str = None, is_async: bool = False -): - print(app_name, db_type, is_async) - create_nest_app(app_name=app_name, db_type=db_type, is_async=is_async) - - -# Create a new group for generating boilerplate -@nest_cli.group("g", short_help="Generate boilerplate code.") -def generate(): - pass - - -@generate.command( - name="module", - help="Generate a new module (controller, service, entity, model, module).", -) -@MODULE_NAME -def generate_module(name: str): - create_nest_module(name=name) +if __name__ == "__main__": + nest_cli() diff --git a/nest/cli/src/__init__.py b/nest/cli/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nest/cli/src/app_controller.py b/nest/cli/src/app_controller.py new file mode 100644 index 0000000..55d3163 --- /dev/null +++ b/nest/cli/src/app_controller.py @@ -0,0 +1,18 @@ +from nest.cli.src.app_service import AppService +from nest.core.decorators.cli.cli_decorators import CliCommand, CliController + + +@CliController("app") +class AppController: + def __init__(self, app_service: AppService): + self.app_service = app_service + + @CliCommand("info") + def get_app_info(self): + app_info = self.app_service.get_app_info() + print(app_info) + + @CliCommand("version", help="Get the version of the app") + def get_app_version(self): + app_info = self.app_service.get_app_info() + print(app_info["app_version"]) diff --git a/nest/cli/src/app_module.py b/nest/cli/src/app_module.py new file mode 100644 index 0000000..b16e234 --- /dev/null +++ b/nest/cli/src/app_module.py @@ -0,0 +1,17 @@ +from nest.cli.src.app_controller import AppController +from nest.cli.src.app_service import AppService +from nest.cli.src.generate.generate_module import GenerateModule +from nest.core import Module +from nest.core.cli_factory import CLIAppFactory + + +@Module( + imports=[GenerateModule], + controllers=[AppController], + providers=[AppService], +) +class AppModule: + pass + + +nest_cli = CLIAppFactory().create(AppModule) diff --git a/nest/cli/src/app_service.py b/nest/cli/src/app_service.py new file mode 100644 index 0000000..d7cc9a1 --- /dev/null +++ b/nest/cli/src/app_service.py @@ -0,0 +1,12 @@ +from nest import __version__ +from nest.core import Injectable + + +@Injectable +class AppService: + def __init__(self): + self.app_name = "PyNest CLI App" + self.app_version = __version__ + + def get_app_info(self): + return {"app_name": self.app_name, "app_version": self.app_version} diff --git a/nest/cli/src/generate/__init__.py b/nest/cli/src/generate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nest/cli/src/generate/generate_controller.py b/nest/cli/src/generate/generate_controller.py new file mode 100644 index 0000000..381dc84 --- /dev/null +++ b/nest/cli/src/generate/generate_controller.py @@ -0,0 +1,43 @@ +import click +from click import Option + +from nest.cli.src.generate.generate_model import SharedOptions +from nest.cli.src.generate.generate_service import GenerateService +from nest.core.decorators.cli.cli_decorators import CliCommand, CliController + + +@CliController("generate") +class GenerateController: + def __init__(self, generate_service: GenerateService): + self.generate_service = generate_service + + @CliCommand("resource") + def generate_resource( + self, + name: SharedOptions.NAME, + path: SharedOptions.PATH, + ): + self.generate_service.generate_resource(name, path) + + @CliCommand("controller", help="Generate a new nest controller") + def generate_controller(self, name: SharedOptions.NAME, path: SharedOptions.PATH): + self.generate_service.generate_controller(name, path) + + @CliCommand("service", help="Generate a new nest service") + def generate_service(self, name: SharedOptions.NAME, path: SharedOptions.PATH): + self.generate_service.generate_service(name, path) + + @CliCommand("module", help="Generate a new nest module") + def generate_module(self, name: SharedOptions.NAME): + self.generate_service.generate_module(name) + + @CliCommand("application", help="Generate a new nest application") + def generate_app( + self, + app_name: SharedOptions.APP_NAME, + db_type: SharedOptions.DB_TYPE, + is_async: SharedOptions.IS_ASYNC, + is_cli: SharedOptions.IS_CLI, + ): + click.echo(f"Generating app {app_name}") + self.generate_service.generate_app(app_name, db_type, is_async, is_cli) diff --git a/nest/cli/src/generate/generate_model.py b/nest/cli/src/generate/generate_model.py new file mode 100644 index 0000000..5b118d6 --- /dev/null +++ b/nest/cli/src/generate/generate_model.py @@ -0,0 +1,42 @@ +import click +from click import Option + + +class SharedOptions: + NAME = Option( + ["-n", "--name"], + help="Name of the resource", + required=True, + type=str, + ) + APP_NAME = Option( + ["-n", "--app-name"], + help="Name of the application", + required=True, + type=str, + ) + PATH = Option( + ["-p", "--path"], help="Path of the resource", required=False, type=str + ) + DB_TYPE = Option( + ["-db", "--db-type"], + help="The type of the database (postgresql, mysql, sqlite, or mongo db).", + required=False, + show_choices=True, + type=str, + default=None, + ) + IS_ASYNC = Option( + ["--is-async"], + help="Whether the project should be async or not (only for relational databases).", + required=False, + is_flag=True, + default=False, + ) + IS_CLI = Option( + ["--is-cli"], + help="Whether the project should be a CLI project or not.", + required=False, + is_flag=True, + default=False, + ) diff --git a/nest/cli/src/generate/generate_module.py b/nest/cli/src/generate/generate_module.py new file mode 100644 index 0000000..5d98847 --- /dev/null +++ b/nest/cli/src/generate/generate_module.py @@ -0,0 +1,8 @@ +from nest.cli.src.generate.generate_controller import GenerateController +from nest.cli.src.generate.generate_service import GenerateService +from nest.core import Module + + +@Module(controllers=[GenerateController], providers=[GenerateService]) +class GenerateModule: + pass diff --git a/nest/cli/src/generate/generate_service.py b/nest/cli/src/generate/generate_service.py new file mode 100644 index 0000000..432a73f --- /dev/null +++ b/nest/cli/src/generate/generate_service.py @@ -0,0 +1,145 @@ +from pathlib import Path + +import click +import yaml + +from nest.cli.templates.templates_factory import TemplateFactory +from nest.core import Injectable + + +@Injectable +class GenerateService: + def __init__(self): + self.template_factory = TemplateFactory() + + @staticmethod + def get_metadata(): + config = {"db_type": None, "is_async": False, "is_cli": False} + setting_path = Path(__file__).parent.parent.parent.parent / "settings.yaml" + if setting_path.exists(): + with open(setting_path, "r") as file: + file = yaml.load(file, Loader=yaml.FullLoader) + config = file["config"] + db_type = config["db_type"] + is_async = config["is_async"] + is_cli = config["is_cli"] if "is_cli" in config else False + return db_type, is_async, is_cli + + def get_template(self, module_name: str): + db_type, is_async, is_cli = self.get_metadata() + template = self.template_factory.get_template( + module_name=module_name, db_type=db_type, is_async=is_async, is_cli=is_cli + ) + return template + + def generate_resource(self, name: str, path: str = None): + """ + Create a new nest resource + + :param name: The name of the module + :param path: The path where the module will be created, Must be in a scope of the src folder. + + The files structure are: + β”œβ”€β”€ ... + β”œβ”€β”€ src + β”‚ β”œβ”€β”€ __init__.py + β”‚ β”œβ”€β”€ module_name + β”œβ”€β”€ __init__.py + β”œβ”€β”€ module_name_controller.py + β”œβ”€β”€ module_name_service.py + β”œβ”€β”€ module_name_model.py + β”œβ”€β”€ module_name_entity.py (only for databases) + β”œβ”€β”€ module_name_module.py + + """ + if path is None: + path = Path.cwd() + template = self.get_template(name) + template.generate_module(name, path) + + def generate_controller(self, name: str, path: str = None): + """ + Create a new nest controller + + :param name: The name of the controller + + The files structure are: + β”œβ”€β”€ ... + β”œβ”€β”€ src + β”‚ β”œβ”€β”€ __init__.py + β”‚ β”œβ”€β”€ module_name + β”œβ”€β”€ __init__.py + β”œβ”€β”€ module_name_controller.py + """ + template = self.get_template(name) + if path is None: + path = Path.cwd() + with open(f"{path}/{name}_controller.py", "w") as f: + f.write(template.generate_empty_controller_file()) + + def generate_service(self, name: str, path: str = None): + """ + Create a new nest service + + :param name: The name of the service + + The files structure are: + β”œβ”€β”€ ... + β”œβ”€β”€ src + β”‚ β”œβ”€β”€ __init__.py + β”‚ β”œβ”€β”€ module_name + β”œβ”€β”€ __init__.py + β”œβ”€β”€ module_name_service.py + """ + template = self.get_template(name) + if path is None: + path = Path.cwd() + with open(f"{path}/{name}_service.py", "w") as f: + f.write(template.generate_empty_service_file()) + + def generate_module(self, name: str, path: str = None): + """ + Create a new nest module + + :param name: The name of the module + :param path: The path where the module will be created + + The files structure are: + β”œβ”€β”€ ... + β”œβ”€β”€ src + β”‚ β”œβ”€β”€ __init__.py + β”‚ β”œβ”€β”€ module_name + β”œβ”€β”€ __init__.py + β”œβ”€β”€ module_name_module.py + """ + template = self.get_template(name) + if path is None: + path = Path.cwd() / "src" + with open(f"{path}/{name}_module.py", "w") as f: + f.write(template.generate_empty_module_file()) + + def generate_app(self, app_name: str, db_type: str, is_async: bool, is_cli: bool): + """ + Create a new nest app + + :param app_name: The name of the app + :param db_type: The type of the database + :param is_async: Whether the project should be async or not + :param is_cli: Whether the project should be a CLI project or not + + The files structure are: + β”œβ”€β”€ ... + β”œβ”€β”€ src + β”‚ β”œβ”€β”€ __init__.py + β”‚ β”œβ”€β”€ app_name + β”œβ”€β”€ __init__.py + β”œβ”€β”€ app_name_controller.py + β”œβ”€β”€ app_name_service.py + β”œβ”€β”€ app_name_model.py + β”œβ”€β”€ app_name_entity.py (only for databases) + β”œβ”€β”€ app_name_module.py + """ + template = self.template_factory.get_template( + module_name=app_name, db_type=db_type, is_async=is_async, is_cli=is_cli + ) + template.generate_project(app_name) diff --git a/nest/common/templates/__init__.py b/nest/cli/templates/__init__.py similarity index 92% rename from nest/common/templates/__init__.py rename to nest/cli/templates/__init__.py index db19f40..0620de1 100644 --- a/nest/common/templates/__init__.py +++ b/nest/cli/templates/__init__.py @@ -6,6 +6,7 @@ class Database(Enum): MYSQL = "mysql" SQLITE = "sqlite" MONGODB = "mongodb" + CLI = "cli" def __str__(self): return self.value diff --git a/nest/cli/templates/abstract_empty_template.py b/nest/cli/templates/abstract_empty_template.py new file mode 100644 index 0000000..702464e --- /dev/null +++ b/nest/cli/templates/abstract_empty_template.py @@ -0,0 +1,35 @@ +from nest.core.templates.abstract_base_template import AbstractBaseTemplate + + +class AbstractEmptyTemplate(AbstractBaseTemplate): + def __init__(self): + super().__init__(is_empty=True) + + def generate_app_file(self) -> str: + return f"""from nest.core.app import App + from src.examples.examples_module import ExamplesModule + + app = App( + description="Blank PyNest service", + modules=[ + ExamplesModule, + ] + ) + """ + + def generate_controller_file(self, name) -> str: + return f"""from nest.core import Controller, Get, Post, Depends, Delete, Put + + from src.{name}.{name}_service import {self.capitalized_name}Service + from src.{name}.{name}_model import {self.capitalized_name} + + + @Controller("{name}") + class {self.capitalized_name}Controller: + + service: {self.capitalized_name}Service = Depends({self.capitalized_name}Service) + + @Get("/get_{name}") + {self.is_async}def get_{name}(self): + return {self.is_await}self.service.get_{name}() +""" diff --git a/nest/common/templates/base_template.py b/nest/cli/templates/base_template.py similarity index 90% rename from nest/common/templates/base_template.py rename to nest/cli/templates/base_template.py index de8f312..7d490a3 100644 --- a/nest/common/templates/base_template.py +++ b/nest/cli/templates/base_template.py @@ -6,6 +6,7 @@ import astor import black +import click from nest import __version__ @@ -184,9 +185,14 @@ def create_template(path: Path, content: Union[str, Callable]) -> None: """ if callable(content): content = content() - print("Generate file: ", path.stem) with open(path, "w") as f: f.write(content) + f.flush() + file_size = os.path.getsize(path) + message = click.style( + f"CREATE {path.parent.name}/{path.name} ({file_size} bytes)", fg="green" + ) + print(message) @staticmethod def create_folder(path: Path) -> None: @@ -200,6 +206,8 @@ def create_folder(path: Path) -> None: None """ if not os.path.exists(path): + message = click.style(f"CREATE {path.parent.name}/{path.name}", fg="blue") + print(message) os.makedirs(path) @abstractmethod @@ -214,7 +222,7 @@ def print_all_templates(self): print("-" * 100) @staticmethod - def find_target_folder(path: str, target: str = "src"): + def find_target_folder(path: Union[str, Path], target: str = "src"): """ Find the target folder within the specified path. @@ -344,7 +352,7 @@ def create_module(self, module_name: str, src_path: Path): raise NotImplementedError @abstractmethod - def generate_module(self, module_name: str): + def generate_module(self, module_name: str, path: str = None): """ Create a new nest module with the following structure: @@ -356,3 +364,27 @@ def generate_module(self, module_name: str): β”œβ”€β”€ module_name_module.py """ raise NotImplementedError + + def generate_empty_controller_file(self) -> str: + return f"""from nest.core import Controller + +@Controller("{self.module_name}") +class {self.capitalized_module_name}Controller: + ... + """ + + def generate_empty_service_file(self) -> str: + return f"""from nest.core import Injectable + +@Injectable +class {self.capitalized_module_name}Service: + ... + """ + + def generate_empty_module_file(self) -> str: + return f"""from nest.core import Module + +@Module() +class {self.capitalized_module_name}Module: + ... + """ diff --git a/nest/common/templates/blank_template.py b/nest/cli/templates/blank_template.py similarity index 94% rename from nest/common/templates/blank_template.py rename to nest/cli/templates/blank_template.py index 4bed76c..50b1fd2 100644 --- a/nest/common/templates/blank_template.py +++ b/nest/cli/templates/blank_template.py @@ -1,7 +1,7 @@ from abc import ABC from pathlib import Path -from nest.common.templates.base_template import BaseTemplate +from nest.cli.templates.base_template import BaseTemplate class BlankTemplate(BaseTemplate, ABC): @@ -55,7 +55,6 @@ class {self.capitalized_module_name}(BaseModel): def service_file(self): return f"""from .{self.module_name}_model import {self.capitalized_module_name} -from functools import lru_cache from nest.core import Injectable @@ -71,7 +70,6 @@ def get_{self.module_name}(self): def add_{self.module_name}(self, {self.module_name}: {self.capitalized_module_name}): self.database.append({self.module_name}) return {self.module_name} - """ def controller_file(self): @@ -113,9 +111,9 @@ def create_module(self, module_name: str, src_path: Path): module_path / f"{module_name}_service.py", self.service_file() ) self.create_template(module_path / f"{module_name}_model.py", self.model_file()) - self.append_module_to_app(path_to_app_py=src_path / "app_module.py") + self.append_module_to_app(path_to_app_py=f"{src_path / 'app_module.py'}") - def generate_module(self, module_name: str): + def generate_module(self, module_name: str, path: str = None): src_path = self.validate_new_module(module_name) self.create_module(module_name, src_path) diff --git a/nest/cli/templates/cli_templates.py b/nest/cli/templates/cli_templates.py new file mode 100644 index 0000000..2720fcc --- /dev/null +++ b/nest/cli/templates/cli_templates.py @@ -0,0 +1,165 @@ +from pathlib import Path + +from nest.cli.templates.base_template import BaseTemplate +from nest.core.decorators.cli.cli_decorators import CliCommand, CliController + + +class ClickTemplate(BaseTemplate): + def __init__(self, module_name: str): + super().__init__(module_name) + + def app_file(self): + return f"""from nest.core.pynest_factory import PyNestFactory +from nest.core.cli_factory import CLIAppFactory +from nest.core import Module +from src.app_service import AppService +from src.app_controller import AppController + + +@Module( + imports=[], + controllers=[AppController], + providers=[AppService], +) +class AppModule: + pass + + +cli_app = CLIAppFactory().create(AppModule) + +if __name__ == "__main__": + cli_app() +""" + + def module_file(self): + return f"""from nest.core import Module +from src.{self.module_name}.{self.module_name}_controller import {self.module_name.capitalize()}Controller +from src.{self.module_name}.{self.module_name}_service import {self.module_name.capitalize()}Service + + +@Module( + controllers=[{self.module_name.capitalize()}Controller], + providers=[{self.module_name.capitalize()}Service], +) +class {self.module_name.capitalize()}Module: + pass +""" + + def controller_file(self): + return f"""from nest.core.decorators.cli.cli_decorators import CliCommand, CliController +from src.{self.module_name}.{self.module_name}_service import {self.module_name.capitalize()}Service + +@CliController("{self.module_name}") +class {self.module_name.capitalize()}Controller: + def __init__(self, {self.module_name}_service: {self.module_name.capitalize()}Service): + self.{self.module_name}_service = {self.module_name}_service + + @CliCommand("hello") + def hello(self): + self.{self.module_name}_service.hello() +""" + + def service_file(self): + return f"""from nest.core import Injectable + +@Injectable +class {self.module_name.capitalize()}Service: + + + def hello(self): + print("Hello from {self.module_name.capitalize()}Service") +""" + + def settings_file(self): + return f"""config: + db_type: '' + is_async: False + is_cli: True +""" + + def app_controller_file(self): + return f"""from nest.core.decorators.cli.cli_decorators import CliCommand, CliController +from src.app_service import AppService + + +@CliController("app") +class AppController: + def __init__(self, app_service: AppService): + self.app_service = app_service + + @CliCommand("version") + def version(self): + self.app_service.version() + + @CliCommand("info") + def info(self): + self.app_service.info() +""" + + def app_service_file(self): + return f"""from nest.core import Injectable +import click + + +@Injectable +class AppService: + + def version(self): + print(click.style("1.0.0", fg="blue")) + + def info(self): + print(click.style("This is a cli nest app!", fg="green")) +""" + + def create_module(self, module_name: str, src_path: Path): + module_path = src_path / module_name + self.create_folder(module_path) + self.create_template(module_path / "__init__.py", "") + self.create_template( + module_path / f"{module_name}_module.py", self.module_file() + ) + self.create_template( + module_path / f"{module_name}_controller.py", self.controller_file() + ) + self.create_template( + module_path / f"{module_name}_service.py", self.service_file() + ) + self.append_module_to_app(path_to_app_py=f"{src_path / 'app_module.py'}") + + def generate_module(self, module_name: str, path: str = None): + src_path = self.validate_new_module(module_name) + self.create_module(module_name, src_path) + + def generate_project(self, project_name: str): + self.create_template(self.nest_path / "settings.yaml", self.settings_file()) + root = self.base_path / project_name + src_path = root / "src" + self.create_folder(root) + self.create_template(root / "README.md", self.readme_file()) + self.create_template(root / "requirements.txt", self.requirements_file()) + self.create_folder(src_path) + self.create_template(src_path / "__init__.py", "") + self.create_template(src_path / "app_module.py", self.app_file()) + self.create_template(src_path / "app_controller.py", self.app_controller_file()) + self.create_template(src_path / "app_service.py", self.app_service_file()) + + def requirements_file(self): + return f"""pynest-api=={self.version}""" + + def config_file(self): + return "" + + def docker_file(self): + return "" + + def dockerignore_file(self): + return "" + + def entity_file(self): + return "" + + def gitignore_file(self): + return "" + + def model_file(self): + return "" diff --git a/nest/cli/templates/mongo_db_template.py b/nest/cli/templates/mongo_db_template.py new file mode 100644 index 0000000..eb1338c --- /dev/null +++ b/nest/cli/templates/mongo_db_template.py @@ -0,0 +1,95 @@ +from abc import ABC + +from nest.cli.templates.abstract_base_template import AbstractBaseTemplate + + +class MongoDbTemplate(AbstractBaseTemplate, ABC): + def __init__(self, name: str): + self.name = name + self.db_type = "mongodb" + super().__init__(self.name, self.db_type) + + def generate_service_file(self) -> str: + return f"""from src.{self.name}.{self.name}_model import {self.capitalized_name} +from src.{self.name}.{self.name}_entity import {self.capitalized_name} as {self.capitalized_name}Entity +from nest.core.decorators import db_request_handler +from functools import lru_cache + + +@lru_cache() +class {self.capitalized_name}Service: + + @db_request_handler + async def get_{self.name}(self): + return await {self.capitalized_name}Entity.find_all().to_list() + + @db_request_handler + async def add_{self.name}(self, {self.name}: {self.capitalized_name}): + new_{self.name} = {self.capitalized_name}Entity( + **{self.name}.dict() + ) + await new_{self.name}.save() + return new_{self.name}.id + + + @db_request_handler + async def update_{self.name}(self, {self.name}: {self.capitalized_name}): + return await {self.capitalized_name}Entity.find_one_and_update( + {{"id": {self.name}.id}}, {self.name}.dict() + ) + + @db_request_handler + async def delete_{self.name}(self, {self.name}: {self.capitalized_name}): + return await {self.capitalized_name}Entity.find_one_and_delete( + {{"id": {self.name}.id}} + ) +""" + + def generate_orm_config_file(self) -> str: + return f"""from nest.core.database.base_odm import OdmService +from src.examples.examples_entity import Examples +import os +from dotenv import load_dotenv + +load_dotenv() + +config = OdmService( + db_type="{self.db_type}", + 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] +) +""" + + def generate_entity_file(self) -> str: + return f"""from beanie import Document + + +class {self.capitalized_name}(Document): + name: str + + class Config: + schema_extra = {{ + "example": {{ + "name": "Example Name", + }} + }} +""" + + 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} + """ + + def generate_dockerfile(self) -> str: + pass diff --git a/nest/common/templates/mongo_template.py b/nest/cli/templates/mongo_template.py similarity index 96% rename from nest/common/templates/mongo_template.py rename to nest/cli/templates/mongo_template.py index bd4b4f2..c36bdab 100644 --- a/nest/common/templates/mongo_template.py +++ b/nest/cli/templates/mongo_template.py @@ -2,8 +2,8 @@ from abc import ABC from pathlib import Path -from nest.common.templates import Database -from nest.common.templates.orm_template import AsyncORMTemplate +from nest.cli.templates import Database +from nest.cli.templates.orm_template import AsyncORMTemplate class MongoTemplate(AsyncORMTemplate, ABC): @@ -130,7 +130,7 @@ def add_document_to_odm_config(self, config_file: Path): self.save_file_with_astor(config_file, tree) self.format_with_black(config_file) - def generate_module(self, module_name: str): + def generate_module(self, module_name: str, path: str = None): src_path = self.validate_new_module(module_name) config_file = self.validate_config_file(src_path) self.create_module( diff --git a/nest/common/templates/mysql_template.py b/nest/cli/templates/mysql_template.py similarity index 92% rename from nest/common/templates/mysql_template.py rename to nest/cli/templates/mysql_template.py index f7d23a5..83a22f7 100644 --- a/nest/common/templates/mysql_template.py +++ b/nest/cli/templates/mysql_template.py @@ -1,7 +1,7 @@ from abc import ABC -from nest.common.templates import Database -from nest.common.templates.orm_template import AsyncORMTemplate, ORMTemplate +from nest.cli.templates import Database +from nest.cli.templates.orm_template import AsyncORMTemplate, ORMTemplate class MySQLTemplate(ORMTemplate, ABC): diff --git a/nest/common/templates/orm_template.py b/nest/cli/templates/orm_template.py similarity index 98% rename from nest/common/templates/orm_template.py rename to nest/cli/templates/orm_template.py index 13ef06d..5230e4a 100644 --- a/nest/common/templates/orm_template.py +++ b/nest/cli/templates/orm_template.py @@ -1,8 +1,8 @@ from abc import ABC, abstractmethod from pathlib import Path -from nest.common.templates import Database -from nest.common.templates.base_template import BaseTemplate +from nest.cli.templates import Database +from nest.cli.templates.base_template import BaseTemplate class ORMTemplate(BaseTemplate, ABC): @@ -175,7 +175,7 @@ def create_module(self, module_name: str, src_path: Path): ) self.append_module_to_app(src_path / "app_module.py") - def generate_module(self, module_name: str): + def generate_module(self, module_name: str, path: str = None): src_path = self.validate_new_module(module_name) self.validate_config_file(src_path) self.create_module(module_name, src_path) diff --git a/nest/common/templates/postgres_template.py b/nest/cli/templates/postgres_template.py similarity index 93% rename from nest/common/templates/postgres_template.py rename to nest/cli/templates/postgres_template.py index c4c751b..ced9957 100644 --- a/nest/common/templates/postgres_template.py +++ b/nest/cli/templates/postgres_template.py @@ -1,7 +1,7 @@ from abc import ABC -from nest.common.templates import Database -from nest.common.templates.orm_template import AsyncORMTemplate, ORMTemplate +from nest.cli.templates import Database +from nest.cli.templates.orm_template import AsyncORMTemplate, ORMTemplate class PostgresqlTemplate(ORMTemplate, ABC): diff --git a/nest/cli/templates/relational_db_template.py b/nest/cli/templates/relational_db_template.py new file mode 100644 index 0000000..f915076 --- /dev/null +++ b/nest/cli/templates/relational_db_template.py @@ -0,0 +1,123 @@ +from abc import ABC + +from nest import __version__ as version +from nest.cli.templates.abstract_base_template import AbstractBaseTemplate + + +class RelationalDBTemplate(AbstractBaseTemplate, ABC): + def __init__(self, name, db_type): + super().__init__(name, db_type) + + def generate_service_file(self) -> str: + return f"""from src.{self.name}.{self.name}_model import {self.capitalized_name} +from src.{self.name}.{self.name}_entity import {self.capitalized_name} as {self.capitalized_name}Entity +from orm_config import config +from nest.core.decorators import db_request_handler +from functools import lru_cache + + +@lru_cache() +class {self.capitalized_name}Service: + + def __init__(self): + self.orm_config = config + self.session = self.orm_config.get_db() + + @db_request_handler + def add_{self.name}(self, {self.name}: {self.capitalized_name}): + new_{self.name} = {self.capitalized_name}Entity( + **{self.name}.dict() + ) + self.session.add(new_{self.name}) + self.session.commit() + return new_{self.name}.id + + @db_request_handler + def get_{self.name}(self): + return self.session.query({self.capitalized_name}Entity).all() + + @db_request_handler + def delete_{self.name}(self, {self.name}_id: int): + self.session.query({self.capitalized_name}Entity).filter_by(id={self.name}_id).delete() + self.session.commit() + return {self.name}_id + + @db_request_handler + def update_{self.name}(self, {self.name}_id: int, {self.name}: {self.capitalized_name}): + self.session.query({self.capitalized_name}Entity).filter_by(id={self.name}_id).update( + {self.name}.dict() + ) + self.session.commit() + return {self.name}_id + """ + + def generate_entity_file(self) -> str: + return f"""from orm_config import config +from sqlalchemy import Column, Integer, String, Float + + +class {self.capitalized_name}(config.Base): + __tablename__ = "{self.name}" + + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String, unique=True) + """ + + def generate_requirements_file(self) -> str: + return f"""anyio==3.6.2 +click==8.1.3 +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 +pynest-api=={version} + """ + + def generate_dockerfile(self) -> str: + pass + + def generate_orm_config_file(self) -> str: + base_template = f"""from nest.core.database.base_orm import OrmService +import os +from dotenv import load_dotenv + +load_dotenv() + + + """ + + if self.db_type == "sqlite": + return f"""{base_template} + config = OrmService( + db_type="{self.db_type}", + config_params=dict( + db_name=os.getenv("SQLITE_DB_NAME", "{self.name}_db"), + ) + ) + """ + else: + return f"""{base_template} + config = OrmService( + db_type="{self.db_type}", + config_params=dict( + host=os.getenv("{self.db_type.upper()}_HOST"), + db_name=os.getenv("{self.db_type.upper()}_DB_NAME"), + user=os.getenv("{self.db_type.upper()}_USER"), + password=os.getenv("{self.db_type.upper()}_PASSWORD"), + port=int(os.getenv("{self.db_type.upper()}_PORT")), + ) + ) + """ + + +if __name__ == "__main__": + relational_db_template = RelationalDBTemplate("users", "mysql") + print(relational_db_template.generate_orm_config_file()) diff --git a/nest/common/templates/sqlite_template.py b/nest/cli/templates/sqlite_template.py similarity index 92% rename from nest/common/templates/sqlite_template.py rename to nest/cli/templates/sqlite_template.py index edc2409..28e29fe 100644 --- a/nest/common/templates/sqlite_template.py +++ b/nest/cli/templates/sqlite_template.py @@ -1,7 +1,7 @@ from abc import ABC -from nest.common.templates import Database -from nest.common.templates.orm_template import AsyncORMTemplate, ORMTemplate +from nest.cli.templates import Database +from nest.cli.templates.orm_template import AsyncORMTemplate, ORMTemplate class SQLiteTemplate(ORMTemplate, ABC): diff --git a/nest/common/templates/templates_factory.py b/nest/cli/templates/templates_factory.py similarity index 66% rename from nest/common/templates/templates_factory.py rename to nest/cli/templates/templates_factory.py index 502dadf..b79f970 100644 --- a/nest/common/templates/templates_factory.py +++ b/nest/cli/templates/templates_factory.py @@ -1,15 +1,16 @@ from typing import Optional, Union -from nest.common.templates import Database -from nest.common.templates.base_template import BaseTemplate -from nest.common.templates.blank_template import BlankTemplate -from nest.common.templates.mongo_template import MongoTemplate -from nest.common.templates.mysql_template import AsyncMySQLTemplate, MySQLTemplate -from nest.common.templates.postgres_template import ( +from nest.cli.templates import Database +from nest.cli.templates.base_template import BaseTemplate +from nest.cli.templates.blank_template import BlankTemplate +from nest.cli.templates.cli_templates import ClickTemplate +from nest.cli.templates.mongo_template import MongoTemplate +from nest.cli.templates.mysql_template import AsyncMySQLTemplate, MySQLTemplate +from nest.cli.templates.postgres_template import ( AsyncPostgresqlTemplate, PostgresqlTemplate, ) -from nest.common.templates.sqlite_template import AsyncSQLiteTemplate, SQLiteTemplate +from nest.cli.templates.sqlite_template import AsyncSQLiteTemplate, SQLiteTemplate class TemplateFactory: @@ -18,7 +19,10 @@ def get_template( db_type: Union[Database, str, None], module_name: str, is_async: Optional[bool] = False, + is_cli: Optional[bool] = False, ) -> BaseTemplate: + if is_cli: + return ClickTemplate(module_name=module_name) if not db_type: return BlankTemplate(module_name=module_name) elif db_type == Database.POSTGRESQL.value: diff --git a/nest/core/__init__.py b/nest/core/__init__.py index 56678b7..50a4cc8 100644 --- a/nest/core/__init__.py +++ b/nest/core/__init__.py @@ -4,14 +4,13 @@ Controller, Delete, Get, + HttpCode, Injectable, Module, Patch, Post, Put, - HttpCode, ) - from nest.core.pynest_application import PyNestApp from nest.core.pynest_container import PyNestContainer from nest.core.pynest_factory import PyNestFactory diff --git a/nest/core/cli_factory.py b/nest/core/cli_factory.py new file mode 100644 index 0000000..99284be --- /dev/null +++ b/nest/core/cli_factory.py @@ -0,0 +1,32 @@ +import asyncio + +import click + +from nest.core.pynest_container import PyNestContainer +from nest.core.pynest_factory import AbstractPyNestFactory, ModuleType + + +class CLIAppFactory(AbstractPyNestFactory): + def __init__(self): + super().__init__() + + def create(self, app_module: ModuleType, **kwargs): + container = PyNestContainer() + container.add_module(app_module) + + cli_app = click.Group("main") + for module in container.modules.values(): + for controller in module.controllers.values(): + for command in controller._cli_group.commands.values(): + original_callback = command.callback + if asyncio.iscoroutinefunction(original_callback): + command.callback = self._run_async(original_callback) + cli_app.add_command(controller._cli_group) + return cli_app + + @staticmethod + def _run_async(coro): + def wrapper(*args, **kwargs): + return asyncio.run(coro(*args, **kwargs)) + + return wrapper diff --git a/nest/core/decorators/__init__.py b/nest/core/decorators/__init__.py index a851e07..1ed7489 100644 --- a/nest/core/decorators/__init__.py +++ b/nest/core/decorators/__init__.py @@ -1,5 +1,5 @@ from nest.core.decorators.controller import Controller -from nest.core.decorators.http_method import Delete, Get, Patch, Post, Put from nest.core.decorators.http_code import HttpCode +from nest.core.decorators.http_method import Delete, Get, Patch, Post, Put from nest.core.decorators.injectable import Injectable from nest.core.decorators.module import Module diff --git a/nest/core/decorators/cli/__init__.py b/nest/core/decorators/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nest/core/decorators/cli/cli_decorators.py b/nest/core/decorators/cli/cli_decorators.py new file mode 100644 index 0000000..3d82011 --- /dev/null +++ b/nest/core/decorators/cli/cli_decorators.py @@ -0,0 +1,51 @@ +from functools import partial + +import click + +from nest.core import Controller +from nest.core.decorators.utils import ( + get_instance_variables, + parse_dependencies, + parse_params, +) + + +def CliController(name: str, **kwargs): + def decorator(cls): + dependencies = parse_dependencies(cls) + setattr(cls, "__dependencies__", dependencies) + non_dep = get_instance_variables(cls) + for key, value in non_dep.items(): + setattr(cls, key, value) + try: + delattr(cls, "__init__") + except AttributeError: + raise AttributeError("Class must have an __init__ method") + + cli_group = click.Group(name, **kwargs) + setattr(cls, "_cli_group", cli_group) + + for attr_name, method in cls.__dict__.items(): + if callable(method) and hasattr(method, "_cli_command"): + params = parse_params(method) + cli_command = click.Command( + name=method._cli_command.name, + callback=partial(method, cls), + params=params, + ) + cli_group.add_command(cli_command) + + return cls + + return decorator + + +def CliCommand(name: str, **kwargs): + def decorator(func): + params = parse_params(func) + func._cli_command = click.Command( + name, callback=func, params=params, help=kwargs.get("help", None) + ) + return func + + return decorator diff --git a/nest/core/decorators/controller.py b/nest/core/decorators/controller.py index a23f05c..8073083 100644 --- a/nest/core/decorators/controller.py +++ b/nest/core/decorators/controller.py @@ -1,10 +1,10 @@ -from typing import Type, Optional +from typing import Optional, Type from fastapi.routing import APIRouter from nest.core.decorators.class_based_view import class_based_view as ClassBasedView -from nest.core.decorators.utils import get_instance_variables, parse_dependencies from nest.core.decorators.http_method import HTTPMethod +from nest.core.decorators.utils import get_instance_variables, parse_dependencies def Controller(prefix: Optional[str] = None, tag: Optional[str] = None): diff --git a/nest/core/decorators/injectable.py b/nest/core/decorators/injectable.py index d18e3a7..2e00aea 100644 --- a/nest/core/decorators/injectable.py +++ b/nest/core/decorators/injectable.py @@ -1,7 +1,9 @@ +from typing import Callable, Optional, Type + from injector import inject + from nest.common.constants import DEPENDENCIES, INJECTABLE_NAME, INJECTABLE_TOKEN from nest.core.decorators.utils import parse_dependencies -from typing import Type, Optional, Callable def Injectable(target_class: Optional[Type] = None, *args, **kwargs) -> Callable: diff --git a/nest/core/decorators/utils.py b/nest/core/decorators/utils.py index b6684bb..69fda4b 100644 --- a/nest/core/decorators/utils.py +++ b/nest/core/decorators/utils.py @@ -1,5 +1,8 @@ import ast import inspect +from typing import Callable, List + +import click from nest.common.constants import INJECTABLE_TOKEN @@ -72,3 +75,15 @@ def parse_dependencies(cls): except Exception as e: raise e return dependecies + + +def parse_params(func: Callable) -> List[click.Option]: + signature = inspect.signature(func) + params = [] + for param in signature.parameters.values(): + try: + if param.annotation != param.empty: + params.append(param.annotation) + except Exception as e: + raise e + return params diff --git a/nest/core/pynest_app_context.py b/nest/core/pynest_app_context.py index bcc9689..030d627 100644 --- a/nest/core/pynest_app_context.py +++ b/nest/core/pynest_app_context.py @@ -97,9 +97,7 @@ def select(self, module: T) -> T: module_token_factory = self.container.module_token_factory metadata = self._module_compiler.extract_metadata(module) - token = module_token_factory.create( - metadata["type"], metadata["dynamic_metadata"] - ) + token = module_token_factory.create(metadata["type"]) selected_module = modules_container.get(token) if selected_module is None: raise UnknownModuleException() diff --git a/nest/core/pynest_factory.py b/nest/core/pynest_factory.py index 3332b0e..2d988ae 100644 --- a/nest/core/pynest_factory.py +++ b/nest/core/pynest_factory.py @@ -1,15 +1,21 @@ -from typing import TypeVar, Type +from abc import ABC, abstractmethod +from typing import Type, TypeVar from fastapi import FastAPI from nest.core.pynest_application import PyNestApp from nest.core.pynest_container import PyNestContainer - ModuleType = TypeVar("ModuleType") -class PyNestFactory: +class AbstractPyNestFactory(ABC): + @abstractmethod + def create(self, main_module: Type[ModuleType], **kwargs): + raise NotImplementedError + + +class PyNestFactory(AbstractPyNestFactory): """Factory class for creating PyNest applications.""" @staticmethod diff --git a/requirements-dev.txt b/requirements-dev.txt index a2ea966..aa977bb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,4 +6,5 @@ beanie==1.20.0 click==8.1.6 PyYAML==6.0.1 injector==0.21.0 -pydantic<2.0.0 \ No newline at end of file +pydantic<2.0.0 +astor>=0.8.1 \ No newline at end of file diff --git a/requirements-release.txt b/requirements-release.txt index f9b2d2f..0d30d21 100644 --- a/requirements-release.txt +++ b/requirements-release.txt @@ -5,6 +5,7 @@ motor==3.2.0 beanie==1.20.0 PyYAML==6.0.1 injector==0.21.0 +astor==0.8.1 # package release setuptools diff --git a/requirements-tests.txt b/requirements-tests.txt index 85ab4c5..a17e03c 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -9,3 +9,4 @@ injector>=0.21.0 pydantic<2.0.0 python-dotenv>=1.0.0 uvicorn>=0.23.1 +astor>=0.8.1