Intermediate commit

This commit is contained in:
mdnapo 2024-06-28 12:48:43 +02:00
parent 5ca27f7f72
commit 96b74e6048
6 changed files with 135 additions and 35 deletions

View File

@ -36,7 +36,7 @@ public partial class MycroForge
"", "",
"\tasync def list(self, value: str) -> List[Entity]:", "\tasync def list(self, value: str) -> List[Entity]:",
"\t\tasync with async_session() as session:", "\t\tasync with async_session() as session:",
"\t\t\t# stmt = select(User).where(Entity.value == value)", "\t\t\t# stmt = select(Entity).where(Entity.value == value)",
"\t\t\t# results = (await session.scalars(stmt)).all()", "\t\t\t# results = (await session.scalars(stmt)).all()",
"\t\t\t# return results", "\t\t\t# return results",
"\t\t\tpass", "\t\t\tpass",

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -2,6 +2,8 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.CLI", "MycroForge.CLI\MycroForge.CLI.csproj", "{27EFB015-AFC3-4046-8D9A-DD5C5D3B35E0}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.CLI", "MycroForge.CLI\MycroForge.CLI.csproj", "{27EFB015-AFC3-4046-8D9A-DD5C5D3B35E0}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.Core", "MycroForge.Core\MycroForge.Core.csproj", "{CFF8BD4E-520D-4319-BA80-3F49B5F493BA}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -12,5 +14,9 @@ Global
{27EFB015-AFC3-4046-8D9A-DD5C5D3B35E0}.Debug|Any CPU.Build.0 = Debug|Any CPU {27EFB015-AFC3-4046-8D9A-DD5C5D3B35E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{27EFB015-AFC3-4046-8D9A-DD5C5D3B35E0}.Release|Any CPU.ActiveCfg = Release|Any CPU {27EFB015-AFC3-4046-8D9A-DD5C5D3B35E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{27EFB015-AFC3-4046-8D9A-DD5C5D3B35E0}.Release|Any CPU.Build.0 = Release|Any CPU {27EFB015-AFC3-4046-8D9A-DD5C5D3B35E0}.Release|Any CPU.Build.0 = Release|Any CPU
{CFF8BD4E-520D-4319-BA80-3F49B5F493BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CFF8BD4E-520D-4319-BA80-3F49B5F493BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CFF8BD4E-520D-4319-BA80-3F49B5F493BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CFF8BD4E-520D-4319-BA80-3F49B5F493BA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -1,5 +1,6 @@
### Dependencies ### Dependencies
- git
- Docker - Docker
- bash (/bin/bash) - bash (/bin/bash)
- Python 3.10.2 (/usr/bin/python3) - Python 3.10.2 (/usr/bin/python3)

View File

@ -61,29 +61,32 @@ Let's go through these one by one.
### .git ### .git
The project is automatically initialised with git by the `m4g init` command. The `m4g init` command will initialize new projects with git by default.
If you don't want to use git you can run `m4g init <name> --without git`. If you don't want to use git you can pass the option `--without git` to `m4g init`.
### .venv ### .venv
To promote isolation of Python dependencies, the project is initialized with a virtual environment by default. To promote isolation of Python dependencies, new projects are initialized with a virtual environment by default.
TODO: This is a good section to introduce the `m4g hydrate` command.
### api/routers/hello.py ### api/routers/hello.py
This file defines a basic example router, which is imported and mapped in `main.py`. This file defines a basic example router, which is imported and mapped in `main.py`. This router is just an example and
can be removed or modified at you discretion.
### db/engine/async_session.py ### db/engine/async_session.py
This file defines the `async_session` function, which can be used to asynchronously connect to a database. This file defines the `async_session` function, which can be used to open an asynchronous session to a database.
### db/entities/entity_base.py ### db/entities/entity_base.py
This is the automatically generated base class that all database entities will inherit from. This file contains an automatically generated entity base class that derives from the DeclarativeBase.
SQLAlchemy requires a base entity class to properly function. All entities must inherit from this class, so that SQLAlchemy & alembic can track them. The entities directory is also
where all newly generated entities will be stored.
### db/entities/versions ### db/versions
This is where the generated database migrations will be kept. This is where the generated database migrations will be stored.
### db/README ### db/README
@ -92,10 +95,10 @@ This README file is automatically generated by the alembic init command.
### db/env.py ### db/env.py
This is the database environment file that is used by alembic to interact with the database. This is the database environment file that is used by alembic to interact with the database.
If you take a close look at the imports, you'll see that the file has been modified to include the entity metadata, If you take a closer look at the imports, you'll see that the file has been modified to assign `EntityBase.metadata` to
i.e. `EntityBase.metadata` and you should also notice that the `DbSettings` class is used to get the connectionstring. a variable called `target_metadata`, this will allow alembic to track changes in your entities. You'll also find that
Any time you generate a new database entity or create a many-to-many relation between two entities, this file will also the `DbSettings` class is used to get the connectionstring. Any time you generate a new database entity, or create a
be modified. many-to-many relation between two entities, this file will also be modified to include the generated classes.
### db/script.py.mako ### db/script.py.mako
@ -103,15 +106,16 @@ This file is automatically generated by the alembic init command.
### db/settings.py ### db/settings.py
This file defines a class that is responsible for retrieving the connectionstring. This file defines the `DbSettings` class, that is responsible for retrieving the database connectionstring.
You will probably want to modify this class to retrieve the connectionstring from a secret manager at some point.
### .gitignore ### .gitignore
The default .gitignore file/ The default .gitignore file that is generated by the `m4g init` command. Modify this file at your discretion.
### alembic.ini ### alembic.ini
The alembic ini file This file is automatically generated by the alembic init command.
### db.docker-compose.yml ### db.docker-compose.yml
@ -119,13 +123,19 @@ A docker compose file for running a database locally.
### m4g.json ### m4g.json
The m4g config file, this file contains some configs that are used by the CLI, This file contains some configs that are used by the CLI, for example the ports to map to the API and database.
for example the ports to map to the API and database.
### main.py ### main.py
The entrypoint for the application. The entrypoint for the application. When generating entities, many-to-many relations or routers, this file will be
modified to include the generated files.
### requirements.txt ### requirements.txt
The requirements file containing the Python dependencies. The requirements file containing the Python dependencies.
TODO: introduce the `m4g install` & `m4g uninstall` commands.
## Scripting
TODO: Dedicate a section to scripting

View File

@ -1,54 +1,123 @@
# Quick tutorial # Quick tutorial
Summary: We're gonna build a todo app to demonstrate how the `m4g` command line tool works.<br/> We're going to build a simple todo app, to demonstrate the capabilities of the MycroForge CLI.
TODO: Supplement this section. After this tutorial, you should have a solid foundation to start exploring and using MycroForge to develop your
projects.
## General notes
The commands in this tutorial assume that you're running them from a MycroForge root directory.
## Initialize the project ## Initialize the project
Run `m4g init todo-example` to initialize a new project. Open a terminal and `cd` into the directory where your project should be created.
Run `m4g init todo-app` to initialize a new project and open the newly created project by running `code todo-app`.
Open the newly created project by running `code todo-example`, make sure you have `vscode` on you machine before running Make sure you have `vscode` on you machine before running this command. If you prefer using another editor, then
this command. If you prefer using another editor, then manually open the generated folder. manually open the generated `todo-app` folder.
## Setup the database ## Setup the database
### Start the database Our todo app needs to keep track of todos, so it needs a storage mechanism of some sorts. A database should be one
of the first things, if not THE first thing, that comes to mind. Luckily, MycroForge provides you with a locally hosted
database for you project out of the box. This setup, is powered by docker compose and can be examined by opening the
`db.docker-compose.yml` file in the project. Follow along to learn how to use the docker based database when developing
locally.
### Run the database
The first step is to start the database, you can do this by running the following command in a terminal.
`m4g db run` `m4g db run`
This command starts the services defined in the `db.docker-compose.yml` file.
You can verify that the services are up by running `docker container ls`. If everything went well, then the previous
command should output the service names defined in `db.docker-compose.yml`.
If you go to [PhpMyAdmin (i.e. http://localhost:5051)](http://localhost:5051), you should now be able to login with the
following credentials.
- user: root
- pass: password
When you're done developing, you can shut down the local database by running `m4g db stop`
### Create the entities ### Create the entities
`m4g db generate entity Tag -c "description:str:String(255)"` Now that the database is running, we can start to create our entities. Run the commands below to create the `Todo` &
`Tag` entities.
`m4g db generate entity Todo -c "description:str:String(255)" -c "is_done:bool:Boolean()"` `m4g db generate entity Tag --column "description:str:String(255)"`
### Define a many to many relation between Todo & Tag `m4g db generate entity Todo --column "description:str:String(255)" -c "is_done:bool:Boolean()"`
After running these commands, you should find the generated entities in the `db/entities` folder of your project.
You should also see that the `main.py` & `db/env.py` files have been modified to include the newly generated entity.
For more information about the `m4g db generate entity` command, you can run `m4g db generate entity -?`.
### Define a many-to-many relation between Todo & Tag
To allow for relations between `Todo` & `Tag`, we'll define a many-to-many relation between the two entities.
This relation makes sense, because a `Todo` can have many `Tags` and a `Tag` could belong to many `Todos`.
You can generate this relation by running the following command from you terminal.
Creating a one-to-many relation would also make sense, but for the purpose of demonstration we're going to demonstrate
the many-to-many relation, because this one is the most complex, since it requires an additional mapping to be included
in the database schema.
`m4g db link many Todo --to-many Tag` `m4g db link many Todo --to-many Tag`
After running this command you should see that both the `Todo` and `Tag` entities now have a new field referencing the
a `List` containing instances of the other entity.
For more information about the `m4g db link` command try running `m4g db link -?`. Note that you can do the same thing
for all sub commands, so if you want to know more about `m4g db link many` you can simply run `m4g db link many -?` to
examine the command. The same is true for all the other commands as well.
### Generate the migration ### Generate the migration
Now that we've generated our entities, it's time to generate a migration that will apply these changes in the database.
Generate the initial migration by running the following command.
`m4g db generate migration initial_migration` `m4g db generate migration initial_migration`
After running this command, you should see the new migration in the `db/version` directory.
### Apply the migration ### Apply the migration
The last step for the database setup is to actually apply the new migration to the database. This can be done by running
the following command.
`m4g db migrate` `m4g db migrate`
If you inspect the database in [PhpMyAdmin](http://localhost:5051), you should now see a populated schema. After running this command, you should now see a populated schema when visiting [PhpMyAdmin](http://localhost:5051).
If for whatever reason you want to undo the last migration, you can simply run `m4g db rollback`.
To stop the database, you can run `m4g db stop`
## Setup the API ## Setup the API
### Generate CRUD for Tag & Todo ### Generate CRUD for Tag & Todo
Our API should provide use with basic endpoint to manage the `Todo` & `Tag` entities, i.e. CRUD functionality.
Writing this code can be boring, since it's pretty much boilerplate with some custom additions sprinkled here and there.
Fortunately, MycroForge can generate a good chunk of this boring code on your behalf. Run the following commands to
generate CRUD functionality for the `Todo` & `Tag` classes.
`m4g api generate crud Tag` `m4g api generate crud Tag`
`m4g api generate crud Todo` `m4g api generate crud Todo`
After running this command you should see that the `api/requests`,`api/routers` & `api/services` now contain the
relevant classes need to support the generated CRUD functionality. This could should be relatively straightforward, so
we won't dive into it, but feel free to take a break and explore what the generated code actually does. Another thing to
note, is that the generated routers are also automatically included in `main.py`.
### Modify the generated Todo request classes ### Modify the generated Todo request classes
Modify `CreateTodoRequest` in `api/requests/create_todo_request.py`, you might need to import `List` from `typing` Since we have a many-to-many relationship between `Todo` & `Tag`, the generated CRUD functionality isn't quite ready
yet. We need to be able to specify which `Tags` to add to a `Todo` when creating or updating it.
To do this, we will allow for a `tag_ids` field in both the `CreateTodoRequest` & the `UpdateTodoRequest`.
This field will contain the ids of the `Tags` that are associated with a `Todo`.
Modify `CreateTodoRequest` in `api/requests/create_todo_request.py`, you might need to import `List` from `typing`.
```python ```python
# Before # Before
@ -63,7 +132,7 @@ class CreateTodoRequest(BaseModel):
tag_ids: Optional[List[int]] = [] tag_ids: Optional[List[int]] = []
``` ```
Modify `UpdateTodoRequest` in `api/requests/update_todo_request.py`, you might need to import `List` from `typing` Modify `UpdateTodoRequest` in `api/requests/update_todo_request.py`, you might need to import `List` from `typing`.
```python ```python
# Before # Before
@ -81,6 +150,8 @@ class UpdateTodoRequest(BaseModel):
### Modify generated TodoService ### Modify generated TodoService
The `TodoService` will also need to be updated to accomodate the management of `tag_ids`.
Add the following imports in `api/services/todo_service.py`. Add the following imports in `api/services/todo_service.py`.
```python ```python
@ -203,3 +274,6 @@ Modify `TodoService.update`
await session.commit() await session.commit()
return True return True
``` ```
At this point, the app should be ready to test.
TODO: Elaborate!