Skip to content

Commit

Permalink
Reorganize code and some renaming (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahopkins authored Aug 14, 2022
1 parent 0b3ec61 commit 7f5f91c
Show file tree
Hide file tree
Showing 44 changed files with 755 additions and 301 deletions.
122 changes: 1 addition & 121 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,124 +50,4 @@ The docs: [ahopkins.github.io/mayim](https://ahopkins.github.io/mayim/guide/)

## Framework support


### Quart

Mayim can attach to Quart using the `init_app` pattern and will handle setting up Mayim and the lifecycle events.

```python
from quart import Quart
from dataclasses import asdict
from typing import List
from mayim import PostgresExecutor
from model import City
from mayim.extension import QuartMayimExtension

app = Quart(__name__)


class CityExecutor(PostgresExecutor):
async def select_all_cities(
self, limit: int = 4, offset: int = 0
) -> List[City]:
...


ext = QuartMayimExtension(
executors=[CityExecutor],
dsn="postgres://postgres:postgres@localhost:5432/world",
)
ext.init_app(app)


@app.route("/")
async def handler():
executor = CityExecutor()
cities = await executor.select_all_cities()
return {"cities": [asdict(city) for city in cities]}
```


### Sanic

Mayim uses [Sanic Extensions](https://sanic.dev/en/plugins/sanic-ext/getting-started.html) v22.6+ to extend your [Sanic app](https://sanic.dev). It starts Mayim and provides dependency injections into your routes of all of the executors

```python
from typing import List
from dataclasses import asdict
from sanic import Sanic, Request, json
from sanic_ext import Extend
from mayim import Mayim
from mayim.executor import Executor
from mayim.extensions import MayimExtension


class CityExecutor(Executor):
async def select_all_cities(
self, limit: int = 4, offset: int = 0
) -> List[City]:
...


app = Sanic(__name__)
Extend.register(
MayimExtension(
executors=[CityExecutor], dsn="postgres://..."
)
)


@app.get("/")
async def handler(request: Request, executor: CityExecutor):
cities = await executor.select_all_cities()
return json({"cities": [asdict(city) for city in cities]})
```


### Starlette

Mayim can attach to Starlette using the `init_app` pattern and will handle setting up Mayim and the lifecycle events.

```python
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
from dataclasses import asdict
from typing import List
from mayim import PostgresExecutor
from model import City

from mayim.extension import StarletteMayimExtension
from mayim.extension.statistics import SQLCounterMixin


class CityExecutor(
SQLCounterMixin,
PostgresExecutor,
):
async def select_all_cities(
self, limit: int = 4, offset: int = 0
) -> List[City]:
...


ext = StarletteMayimExtension(
executors=[CityExecutor],
dsn="postgres://postgres:postgres@localhost:5432/world",
)


async def handler(request):
executor = CityExecutor()
cities = await executor.select_all_cities()
return JSONResponse({"cities": [asdict(city) for city in cities]})


app = Starlette(
debug=True,
routes=[
Route("/", handler),
],
)
ext.init_app(app)
```
Out of the box, Mayim comes with extensions to support Quart, Sanic, and Starlette applications. Checkout the docs for more info.
3 changes: 2 additions & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"devDependencies": {
"vue-tabs-component": "^1.5.0",
"vuepress": "^1.5.3",
"vuepress-plugin-tabs": "^0.3.0"
"vuepress-plugin-tabs": "^0.3.0",
"vuepress-plugin-code-copy": "^1.0.6"
}
}
5 changes: 5 additions & 0 deletions docs/src/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const pages = [
'simple',
'executors',
'hydrators',
'extensions',
]
module.exports = {
/**
Expand Down Expand Up @@ -80,5 +81,9 @@ module.exports = {
'@vuepress/plugin-back-to-top',
'@vuepress/plugin-medium-zoom',
"tabs",
[
"vuepress-plugin-code-copy",
{ color: "#5dadec", backgroundTransition: false },
],
]
}
32 changes: 19 additions & 13 deletions docs/src/guide/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,55 @@

## :droplet: What is Mayim?

The simplest way to describe it would be to call it a **one-way ORM**. That is to say that it does *not* craft SQL statements for you. Think of it as **BYOQ** (Bring Your Own Query).
The simplest way to describe it would be to call it a **one-way ORM**. That is to say that it does *not* craft SQL statements for you. But it does take your executed query results and map them back to objects.

Think of it as **BYOQ** (Bring Your Own Query) mapping utility.

You supply the query, it handles the execution and model hydration.

## :droplet: Why?

I have nothing against ORMs, truthfully. They serve a great purpose and can be the right tool for the job in many situations. I just prefer not to use them where possible. Instead, I would rather **have control of my SQL statements**.

The typical tradeoff though is that there is more work needed to hydrate from SQL queries to objects. Mayim aims to solve that.
The typical tradeoff though is that there is more work needed to hydrate from SQL queries to objects. Without an ORM, it is generally more difficult to maintain your code base as your schema changes.

Mayim aims to solve that by providing an **elegant API** with typed objects and methods. Mayim fully embraces **type annotations** and encourages their usage.

## :droplet: How?

There are two parts to it:

1. Write some SQL files in a location that Mayim can access at startup
1. Write some SQL in a location that Mayim can access at startup (_this can be in a decorator as shown below, or `.sql` files as seen later on_)
1. Create an `Executor` that defines the query parameters that will be passed to your SQL

Here is a real simple example:

```python
import asyncio
from typing import List
from mayim import Mayim, PostgresExecutor, sql
from mayim import Mayim, SQLiteExecutor, query
from dataclasses import dataclass


@dataclass
class Person:
name: str

class PersonExecutor(PostgresExecutor):
@sql("SELECT * FROM people LIMIT $limit OFFSET $offset")
async def select_all_people(
self, limit: int = 4, offset: int = 0
) -> List[Person]:

class PersonExecutor(SQLiteExecutor):
@query("SELECT $name as name")
async def select_person(self, name: str) -> Person:
...


async def run():
executor = PersonExecutor()
Mayim(dsn="postgres://...")
print(await executor.select_all_cities())
Mayim(db_path="./example.db")
print(await executor.select_person(name="Adam"))


asyncio.run(run())
```

This example should be complete and run as is. Of course, you would need to run a Postgres instance somewhere and set the `dsn` string appropriately. Other than that, this should be all set.
This example should be complete and run as is.

Let's continue on to see how we can install it. :sunglasses:
10 changes: 5 additions & 5 deletions docs/src/guide/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ We will get into it in [more detail later](executors), but the `Executor` is the

You need to:

- subclass `Executor` (more likely you want one of its subclasses: `PostgresExecutor` or `MysqlExecutor`);
- create method definitions that match the names of your SQL statements (yes, those methods will likely be empty);
- name the arguments that will be injected into the query; and
- provide the model you want to be returned as the return annotation (or annotate it as a `Dict` if that is what you want back).
- **subclass** `Executor` (more likely you want one of its subclasses: `PostgresExecutor`, `MysqlExecutor`, `SQLiteExecutor`);
- create **method definitions** that match the names of your SQL statements (yes, those methods will likely be empty as seen in the snippet below);
- name the **arguments** that will be injected into the query; and
- provide the model you want to be returned as the **return annotation** (or annotate it as a `Dict` if that is what you want back).


```python
Expand All @@ -53,7 +53,7 @@ class CityExecutor(PostgresExecutor):

**Usually**, your executor will have a bunch of empty methods. That is because Mayim will automatically generate the code needed to run the SQL statement and return the object specified in the return annotation.

In this case, it will try to execute the SQL query called `./queries/select_all_cities.sql` (see [more on writing SQL files](sqlfiles)). Then, it will try and turn the result into a list of `City` objects.
In this case, since `select_all_cities` is an empty method, Mayim will try to execute the SQL query called `./queries/select_all_cities.sql` (see [more on writing SQL files](sqlfiles)). Then, it will try and turn the result into a list of `City` objects because of the method's return annotation.

## Empty `Executor` methods

Expand Down
60 changes: 52 additions & 8 deletions docs/src/guide/executors.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@ async def run():

### Implied registration

On the frontpage example, we instantiated our executor before calling `Mayim`, and **never** explicitly loaded the class (or instance). This is fine *only if it is instantiated before the `Mayim` object*. Because of this "gotcha", this implied registration option is not the recommended approach.
On the frontpage example, we instantiated our executor before calling `Mayim`, and **never** explicitly loaded the class (or instance). This implie registration is fine *only if it is instantiated before the `Mayim` object*. Because of this "gotcha", this option is not the recommended approach.


### Global registration with `@register`

You also have the option of wrapping your `Executor` instance with `@register`. This will automatically add the `Executor` to the registry without having to manually load it later.
You also have the option of wrapping your `Executor` instance with `@register`. This will automatically add the `Executor` to the registry without having to manually load it later. This is more explicit (and therefore preferred) than implied registration.

```python
from mayim import PostgresExecutor, register
Expand All @@ -90,7 +90,7 @@ async def run():
```

::: warning
Be careful with this method. If you use it you may need to pat close attention to your import ordering. That is because you will need to either: (1) instantiate `Mayim`, or (2) run `mayim.load` sometime after the `Executor` has been imported.
Be careful with this method. If you use it you may need to pay close attention to your import ordering. That is because you will need to either: (1) instantiate `Mayim`, or (2) run `mayim.load` sometime after the `Executor` has been imported.

If you are using `@register` and your queries are not running as expected, a first place to check is to make sure that your imports are properly ordered.
:::
Expand Down Expand Up @@ -124,11 +124,12 @@ async def handler(request: Request):
Sometimes you may decide that you do not want to have to [load SQL from files](sqlfiles). In this case, you can define the SQL in your Python code.

```python
from mayim import PostgresExecutor, sql
from mayim import PostgresExecutor, query

class ItemExecutor(PostgresExecutor):
@sql(
"""SELECT *
@query(
"""
SELECT *
FROM items
WHERE item_id = $item_id;
"""
Expand All @@ -152,10 +153,32 @@ class CityExecutor(PostgresExecutor):
query += "WHERE id = $ident"
else:
query += "WHERE name = $ident"
return await self.execute(query, as_list=False, params={"ident": ident})
return await self.execute(
query,
as_list=False,
allow_none=False,
params={"ident": ident}
)
```

*FYI - `as_list` defaults to `False`. It is shown here just as an example that you may need to be explicit about passing this argument if you expect to return a `list`. Once you have dropped down into executing your own code, you are responsible for telling Mayim if it needs to return a `list` or a single instance.*
*FYI - `as_list` defaults to `False`. It is shown here just as an example that you may need to be explicit about passing this argument if you expect to return a `list`. Once you have dropped down into executing your own code, you are responsible for telling Mayim if it needs to return a `list` or a single instance. Similarly, `allow_none` also defaults to `False` and is shown for demonstration purposes here.*

### Query fragments

What if you have some really big query fragments, or some fragment that needs to be used in a lot of places? For example, you might have a large select statement that you want to reuse. Wouldn't it be nice if you could define those in `.sql` files and compose them? Of course!

```python
class CityExecutor(PostgresExecutor):
generic_prefix: str = "fragment_"

async def select_city(self, ident: int | str, by_id: bool) -> City:
query = self.get_query("fragment_select_city")
if by_id:
query += self.get_query("fragment_where_id")
else:
query += self.get_query("fragment_where_name")
return await self.execute(query, params={"ident": ident})
```

## Low level `run_sql`

Expand All @@ -174,6 +197,8 @@ class CityExecutor(PostgresExecutor):

In the previous example, you may have noticed `query = self.get_query()`. This method allows you to fetch the predefined query that *would* have been executed. It is helpful in cases where you need to add some more custom logic to your method, but still want to preload your SQL from a `.sql` file.

When you call `get_query()` with no arguments, it will fetch the query that Mayim thinks should have been run. This is based upon the method name. You can explicitly pass it a name to retrieve that as seen in the [query fragments](#query-fragments) section.

## Custom pools

Sometimes, you may find the need for an executor to be linked to a different database pool than other executors. This might be particularly helpful if you have multiple databases to query.
Expand Down Expand Up @@ -254,3 +279,22 @@ class CityExecutor(PostgresExecutor):
results = await self.run_sql(query.text, params={"city_id": city_id})
return [hydrator.hydrate(city, City) for city in results]
```

## Empty methods in STRICT mode

By default, Mayim will startup in STRICT mode. You can disable this at instantiation, or `load`:

```python
mayim = Mayim(..., strict=False)
# OR
mayim.load(..., strict=False)
```

What does it do, and why would you use it? When enabled, Mayim will raise an exception when it encounters an empty Executor method without a defined query. So, if you have a method without any code inside of it, without a `@query` decorator, and without a `.sql` file, you will receive an exception.

```python
# Loading this executor would raise an exception in STRICT mode
class CityExecutor(PostgresExecutor):
async def select_query_does_not_exist(self) -> None:
...
```
Loading

0 comments on commit 7f5f91c

Please sign in to comment.