@@ -19,47 +19,59 @@ Services provide:
1919- Automatic schema validation and transformation
2020- Support for SQLAlchemy query results (Row types) and RowMapping objects
2121
22+ .. note ::
23+
24+ The following example assumes the existence of the
25+ ``Post `` model defined in :ref: `many_to_many_relationships ` and the
26+ ``Tag `` model defined in :ref: `using_unique_mixin `.
27+
2228Basic Service Usage
2329-------------------
2430
25- Let's build upon our blog example by creating services for posts and tags :
31+ Let's build upon our blog example by creating services for posts:
2632
2733.. code-block :: python
2834
2935 import datetime
30- from typing import Optional, List
31- from uuid import UUID
36+ from typing import Optional
3237
38+ from advanced_alchemy.repository import SQLAlchemyAsyncRepository
3339 from advanced_alchemy.service import SQLAlchemyAsyncRepositoryService
3440 from pydantic import BaseModel
3541
3642 # Pydantic schemas for validation
3743 class PostCreate (BaseModel ):
3844 title: str
3945 content: str
40- tag_names: List[str ]
46+ tag_names: list[str ]
47+
4148
4249 class PostUpdate (BaseModel ):
4350 title: Optional[str ] = None
4451 content: Optional[str ] = None
4552 published: Optional[bool ] = None
4653
54+
4755 class PostResponse (BaseModel ):
4856 id : int
4957 title: str
5058 content: str
5159 published: bool
52- published_at: Optional[datetime.datetime]
5360 created_at: datetime.datetime
5461 updated_at: datetime.datetime
55- tags: List[" TagResponse" ]
5662
5763 model_config = {" from_attributes" : True }
5864
65+
5966 class PostService (SQLAlchemyAsyncRepositoryService[Post]):
60- """ Service for managing blog posts with automatic schema validation."""
67+ """ Post Service."""
68+
69+ class Repo (SQLAlchemyAsyncRepository[Post]):
70+ """ Post repository."""
6171
62- repository_type = PostRepository
72+ model_type = Post
73+
74+ repository_type = Repo
6375
6476 Service Operations
6577------------------
@@ -68,28 +80,17 @@ Services provide high-level methods for common operations:
6880
6981.. code-block :: python
7082
71- async def create_post (
72- post_service : PostService,
73- data : PostCreate,
74- ) -> PostResponse:
75- """ Create a post with associated tags."""
76- post = await post_service.create(
77- data,
78- auto_commit = True ,
79- )
83+ async def create_post (post_service : PostService, data : PostCreate) -> PostResponse:
84+ post = await post_service.create(data = data, auto_commit = True )
8085 return post_service.to_schema(post, schema_type = PostResponse)
8186
87+
8288 async def update_post (
8389 post_service : PostService,
8490 post_id : int ,
8591 data : PostUpdate,
8692 ) -> PostResponse:
87- """ Update a post."""
88- post = await post_service.update(
89- item_id = post_id,
90- data = data,
91- auto_commit = True ,
92- )
93+ post = await post_service.update(item_id = post_id, data = data, auto_commit = True )
9394 return post_service.to_schema(post, schema_type = PostResponse)
9495
9596 Composite Primary Keys
@@ -120,118 +121,109 @@ Complex Operations
120121Services can handle complex business logic involving multiple models.
121122The code below shows a service coordinating posts and tags.
122123
123- .. note ::
124-
125- The following example assumes the existence of the
126- ``Post `` model defined in :ref: `many_to_many_relationships ` and the
127- ``Tag `` model defined in :ref: `using_unique_mixin `.
128-
129124.. code-block :: python
130125
131- from typing import List
132-
133- from advanced_alchemy.exceptions import ErrorMessages
134- from advanced_alchemy.service import SQLAlchemyAsyncRepositoryService
126+ import datetime
127+ from typing import Any
128+ from advanced_alchemy.repository import SQLAlchemyAsyncRepository
129+ from advanced_alchemy.service import SQLAlchemyAsyncRepositoryService, schema_dump
135130 from advanced_alchemy.service.typing import ModelDictT
131+ from advanced_alchemy.filters import LimitOffset
132+ from advanced_alchemy.service.pagination import OffsetPagination
133+ from advanced_alchemy.utils.text import slugify
134+
135+ class PostService (SQLAlchemyAsyncRepositoryService[Post]):
136+ """ Post service for handling post operations with tag management."""
136137
137- from .models import Post, Tag
138+ class Repo (SQLAlchemyAsyncRepository[Post]):
139+ """ Post repository."""
138140
139- class PostService (SQLAlchemyAsyncRepositoryService[Post, PostRepository]):
141+ model_type = Post
140142
141143 loader_options = [Post.tags]
142- repository_type = PostRepository
143- match_fields = [" name" ]
144+ repository_type = Repo
145+ match_fields = [" title" ]
146+
147+ async def to_model_on_create (self , data : ModelDictT[Post]) -> ModelDictT[Post]:
148+ """ Convert and enrich data for post creation, handling tags."""
149+ data = schema_dump(data)
150+ tags_added = data.pop(" tags" , [])
151+ data = await super ().to_model(data)
144152
145- # Override creation behavior to handle tags
146- async def create (self , data : ModelDictT[Post], ** kwargs ) -> Post:
147- """ Create a new post with tags, if provided."""
148- tags_added: list[str ] = []
149- if isinstance (data, dict ):
150- data[" id" ] = data.get(" id" , uuid4())
151- tags_added = data.pop(" tags" , [])
152- data = await self .to_model(data, " create" )
153153 if tags_added:
154154 data.tags.extend(
155155 [
156- await Tag.as_unique_async(self .repository.session, name = tag_text , slug = slugify(tag_text ))
157- for tag_text in tags_added
156+ await Tag.as_unique_async(self .repository.session, name = tag , slug = slugify(tag ))
157+ for tag in tags_added
158158 ],
159159 )
160- return await super ().create(data = data, ** kwargs)
161-
162- # Override update behavior to handle tags
163- async def update (
164- self ,
165- data : ModelDictT[Post],
166- item_id : Any | None = None ,
167- ** kwargs ,
168- ) -> Post:
169- """ Update a post with tags, if provided."""
170- tags_updated: list[str ] = []
171- if isinstance (data, dict ):
172- tags_updated.extend(data.pop(" tags" , None ) or [])
173- data[" id" ] = item_id
174- data = await self .to_model(data, " update" )
175- existing_tags = [tag.name for tag in data.tags]
176- tags_to_remove = [tag for tag in data.tags if tag.name not in tags_updated]
160+ return data
161+
162+ async def to_model_on_update (self , data : ModelDictT[Post]) -> ModelDictT[Post]:
163+ """ Convert and enrich data for post update, handling tags."""
164+ data = schema_dump(data)
165+ tags_updated = data.pop(" tags" , [])
166+ post = await super ().to_model(data)
167+
168+ if tags_updated:
169+ existing_tags = [tag.name for tag in post.tags]
170+ tags_to_remove = [tag for tag in post.tags if tag.name not in tags_updated]
177171 tags_to_add = [tag for tag in tags_updated if tag not in existing_tags]
172+
178173 for tag_rm in tags_to_remove:
179- data.tags.remove(tag_rm)
180- data.tags.extend(
174+ post.tags.remove(tag_rm)
175+
176+ post.tags.extend(
181177 [
182- await Tag.as_unique_async(self .repository.session, name = tag_text , slug = slugify(tag_text ))
183- for tag_text in tags_to_add
178+ await Tag.as_unique_async(self .repository.session, name = tag , slug = slugify(tag ))
179+ for tag in tags_to_add
184180 ],
185181 )
186- return await super ().update(
187- data = data,
188- item_id = item_id,
189- ** kwargs,
190- )
191-
192- # A custom write operation
193- async def publish_post (
194- self ,
195- post_id : int ,
196- publish : bool = True ,
197- ) -> PostResponse:
198- """ Publish or unpublish a post with timestamp."""
199- data = PostUpdate(
200- published = publish,
201- published_at = datetime.datetime.utcnow() if publish else None ,
202- )
203- post = await self .repository.update(
204- item_id = post_id,
205- data = data,
206- auto_commit = True ,
207- )
208- return self .to_schema(post, schema_type = PostResponse)
209-
210- # A custom read operation
211- async def get_trending_posts (
212- self ,
213- days : int = 7 ,
214- min_views : int = 100 ,
215- ) -> List[PostResponse]:
216- """ Get trending posts based on view count and recency."""
217- posts = await self .post_service.list(
218- Post.published == True ,
219- Post.created_at > (datetime.datetime.utcnow() - timedelta(days = days)),
220- Post.view_count >= min_views,
221- order_by = [Post.view_count.desc()],
222- )
223- return self .post_service.to_schema(posts, schema_type = PostResponse)
224-
225- # Override the default `to_model` to handle slugs
226- async def to_model (self , data : ModelDictT[Post], operation : str | None = None ) -> Post:
227- """ Convert a dictionary, msgspec Struct, or Pydantic model to a Post model. """
228- if (is_msgspec_struct(data) or is_pydantic_model(data)) and operation in {" create" , " update" } and data.slug is None :
229- data.slug = await self .repository.get_available_slug(data.name)
230- if is_dict(data) and " slug" not in data and operation == " create" :
182+ return post
183+
184+ Working with Slugs
185+ ------------------
186+
187+ Services can automatically generate URL-friendly slugs using the ``SQLAlchemyAsyncSlugRepository ``.
188+ Here's an example service for managing tags with automatic slug generation:
189+
190+ .. code-block :: python
191+
192+ from advanced_alchemy.repository import SQLAlchemyAsyncSlugRepository
193+ from advanced_alchemy.service import SQLAlchemyAsyncRepositoryService, schema_dump, is_dict_without_field, is_dict_with_field
194+ from advanced_alchemy.service.typing import ModelDictT
195+
196+ class TagService (SQLAlchemyAsyncRepositoryService[Tag]):
197+ """ Tag service with automatic slug generation."""
198+
199+ class Repo (SQLAlchemyAsyncSlugRepository[Tag]):
200+ """ Tag repository."""
201+
202+ model_type = Tag
203+
204+ repository_type = Repo
205+ match_fields = [" name" ]
206+
207+ async def to_model_on_create (self , data : ModelDictT[Tag]) -> ModelDictT[Tag]:
208+ """ Generate slug on tag creation if not provided."""
209+ data = schema_dump(data)
210+ if is_dict_without_field(data, " slug" ) and is_dict_with_field(data, " name" ):
231211 data[" slug" ] = await self .repository.get_available_slug(data[" name" ])
232- if is_dict(data) and " slug" not in data and " name" in data and operation == " update" :
212+ return data
213+
214+ async def to_model_on_update (self , data : ModelDictT[Tag]) -> ModelDictT[Tag]:
215+ """ Update slug if name changes."""
216+ data = schema_dump(data)
217+ if is_dict_without_field(data, " slug" ) and is_dict_with_field(data, " name" ):
233218 data[" slug" ] = await self .repository.get_available_slug(data[" name" ])
234- return await super ().to_model(data, operation)
219+ return data
220+
221+ async def to_model_on_upsert (self , data : ModelDictT[Tag]) -> ModelDictT[Tag]:
222+ """ Generate slug on upsert if needed."""
223+ data = schema_dump(data)
224+ if is_dict_without_field(data, " slug" ) and (tag_name := data.get(" name" )) is not None :
225+ data[" slug" ] = await self .repository.get_available_slug(tag_name)
226+ return data
235227
236228 Schema Integration
237229------------------
@@ -246,15 +238,16 @@ Pydantic Models
246238 from pydantic import BaseModel
247239 from typing import Optional
248240
249- class UserSchema (BaseModel ):
250- name: str
251- email: str
252- age: Optional[int ] = None
241+ class PostSchema (BaseModel ):
242+ id : int
243+ title: str
244+ content: str
245+ published: bool
253246
254247 model_config = {" from_attributes" : True }
255248
256249 # Convert database model to Pydantic schema
257- user_data = service .to_schema(user_model , schema_type = UserSchema )
250+ post_data = post_service .to_schema(post_model , schema_type = PostSchema )
258251
259252 Msgspec Structs
260253***************
@@ -264,13 +257,14 @@ Msgspec Structs
264257 from msgspec import Struct
265258 from typing import Optional
266259
267- class UserStruct (Struct ):
268- name: str
269- email: str
270- age: Optional[int ] = None
260+ class PostStruct (Struct ):
261+ id : int
262+ title: str
263+ content: str
264+ published: bool
271265
272266 # Convert database model to Msgspec struct
273- user_data = service .to_schema(user_model , schema_type = UserStruct )
267+ post_data = post_service .to_schema(post_model , schema_type = PostStruct )
274268
275269 Attrs Classes
276270*************
@@ -281,13 +275,14 @@ Attrs Classes
281275 from typing import Optional
282276
283277 @define
284- class UserAttrs :
285- name: str
286- email: str
287- age: Optional[int ] = None
278+ class PostAttrs :
279+ id : int
280+ title: str
281+ content: str
282+ published: bool
288283
289284 # Convert database model to attrs class
290- user_data = service .to_schema(user_model , schema_type = UserAttrs )
285+ post_data = post_service .to_schema(post_model , schema_type = PostAttrs )
291286
292287 .. note ::
293288
@@ -296,27 +291,6 @@ Attrs Classes
296291 for improved performance and type-aware serialization. This provides better handling of
297292 complex types, nested structures, and custom converters.
298293
299- SQLAlchemy Query Result Support
300- *******************************
301-
302- Services now provide comprehensive support for SQLAlchemy query results:
303-
304- .. code-block :: python
305-
306- from sqlalchemy import select
307-
308- # Direct support for SQLAlchemy Row objects
309- query_results = await session.execute(select(User))
310- rows = query_results.fetchall() # Returns list[Row[Any]]
311-
312- # Convert Row objects to schema types
313- user_data = service.to_schema(rows[0 ], schema_type = UserSchema)
314- users_paginated = service.to_schema(rows, schema_type = UserSchema)
315-
316- # Also supports RowMapping objects
317- row_mapping_results = await session.execute(select(User)).mappings()
318- mapping_data = service.to_schema(row_mapping_results.first(), schema_type = UserSchema)
319-
320294
321295Framework Integration
322296---------------------
0 commit comments