Skip to content

Commit 5fed38a

Browse files
committed
updates
1 parent 864fee6 commit 5fed38a

11 files changed

Lines changed: 626 additions & 50 deletions

File tree

README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# The Object API
2+
3+
ObjectAPI provides a concise negative-boilerplate paradigm for creating full-stack web applications with Python. It is built on top of [FastAPI](https://fastapi.tiangolo.com/), [SQLModel](https://sqlmodel.tiangolo.com/), and [pydantic](https://docs.pydantic.dev/latest/).
4+
5+
## Features
6+
7+
- Active record pattern for database access
8+
- Automatic get_by_id lookup for instance methods with route decorators
9+
- Automatic CRUD routes
10+
- Scheduled service methods
11+
- Managed DB sessions for service methods and for each request
12+
13+
## Installation
14+
15+
```bash
16+
pip install object-api
17+
```
18+
19+
## Usage
20+
21+
```python
22+
from object_api import App, Entity, RouterBuilder, ServiceBuilder
23+
24+
app = App()
25+
26+
class User(Entity):
27+
class Meta:
28+
service = ServiceBuilder()
29+
router = RouterBuilder()
30+
31+
new_private = ["pass"]
32+
33+
name: str
34+
pass: str
35+
age: int
36+
37+
@service.servicemethod
38+
@classmethod
39+
def remove_inactive(cls):
40+
for user in User.get_all():
41+
if user.age > 100:
42+
user.delete()
43+
44+
@router.route()
45+
def get_name(self):
46+
return self.name
47+
48+
@router.post("/change_name")
49+
def change_name(self, name: str):
50+
self.name = name
51+
self.save()
52+
53+
app = App()
54+
55+
app.run()
56+
```
57+
58+
## Documentation
59+
60+
<https://github.com/ComputaCo/object-api>
61+
62+
## License
63+
64+
[MIT](https://choosealicense.com/licenses/mit/)

object_api/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from object_api.entity import Entity
2+
from object_api.app import App
3+
from object_api.router_builder import RouterBuilder
4+
from object_api.service_builder import ServiceBuilder

object_api/app.py

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
from __future__ import annotations
2-
from contextlib import contextmanager
2+
3+
from contextlib import asynccontextmanager, contextmanager
34
from typing import Generator
4-
from fastapi import FastAPI
5-
from pydantic import Field
6-
from sqlalchemy.future.engine import Engine
75

8-
from scheduler import Scheduler
6+
from sqlalchemy.future.engine import Engine
97
from sqlmodel import SQLModel, Session, create_engine
8+
from pydantic import Field
9+
from fastapi import FastAPI
10+
from scheduler import Scheduler
1011

1112
from object_api.entity import Entity
13+
from object_api.request_context import RequestContextMiddleware, get_request_id
1214
from object_api.utils.has_post_init import HasPostInitMixin
1315

1416

1517
class App(FastAPI, HasPostInitMixin):
1618
CURRENT_APP: App = Field(None, init=False)
1719

18-
scheduler: Scheduler = Field(default_factory=Scheduler, init=False)
20+
scheduler: Scheduler = Field(
21+
default_factory=lambda: Scheduler(n_threads=0), init=False
22+
)
1923
entity_classes: list[type[Entity]] = Field([], init=False)
2024
db_engine: Engine = Field(None, init=False)
2125
debug: bool = True
@@ -30,6 +34,7 @@ def __post_init__(self):
3034
)
3135
self.CURRENT_APP = self
3236

37+
self.add_middleware(RequestContextMiddleware)
3338
self.build()
3439

3540
return super().__post_init__()
@@ -50,25 +55,45 @@ def build_routers(self):
5055
for entity_class in self.entity_classes:
5156
entity_class.Meta.router.build_router(entity_class)
5257

53-
_object_api_app_active_session: Session = Field(None, init=False)
58+
# The servicemethods will just have to manually pass the session to their invoked service methods
59+
_per_thread_active_session: dict[str, Session] = Field(None, init=False)
5460

55-
@contextmanager
61+
@asynccontextmanager
5662
async def session(self) -> Generator[None, None, None]:
57-
if self._object_api_app_active_session:
58-
if not self._object_api_app_active_session.is_active:
59-
raise RuntimeError(
60-
"Session is already closed. Please use a new session for each request."
61-
)
62-
63-
yield self._object_api_app_active_session
63+
"""Returns (and possibly creates) a session for the current req-response cycle
64+
or returns a globally shared session if no request context is available."""
65+
req_id = get_request_id() or "global"
66+
67+
# maybe create or re-initialize the session if its non-existent or inactive
68+
if (
69+
req_id not in self._per_thread_active_session
70+
or not self._per_thread_active_session[req_id]
71+
or not self._per_thread_active_session[req_id].is_active
72+
):
73+
self._per_thread_active_session[req_id] = Session(self.db_engine)
74+
75+
# now enter the session context or just yield the session if it's already active
76+
if self._per_thread_active_session[req_id].is_active:
77+
yield self._per_thread_active_session[req_id]
6478
return
79+
else:
80+
with self._per_thread_active_session[req_id] as session:
81+
yield session
82+
# make sure to clean up the session after the request is done
83+
del self._per_thread_active_session[req_id]
84+
return
85+
86+
@asynccontextmanager
87+
@staticmethod
88+
async def current_session() -> Generator[Session, None, None]:
89+
if not App.CURRENT_APP:
90+
raise RuntimeError(
91+
"No current app. Please use App.as_current() to set the current app."
92+
)
6593

66-
with Session(self.db_engine) as session:
67-
self._object_api_app_active_session = session
68-
yield session
69-
self._object_api_app_active_session = None
94+
yield App.CURRENT_APP.session()
7095

71-
@contextmanager
96+
@asynccontextmanager
7297
async def as_current(self) -> Generator[App, None, None]:
7398
old_app = self.CURRENT_APP
7499
self.CURRENT_APP = self

0 commit comments

Comments
 (0)