{"id":13795818,"url":"https://github.com/tinkoffjournal/freddie","last_synced_at":"2025-09-29T11:31:45.220Z","repository":{"id":42189043,"uuid":"267556265","full_name":"tinkoffjournal/freddie","owner":"tinkoffjournal","description":"DRF-like declarative viewsets for FastAPI","archived":true,"fork":false,"pushed_at":"2023-02-01T14:13:02.000Z","size":89,"stargazers_count":67,"open_issues_count":0,"forks_count":6,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-03-14T22:50:51.237Z","etag":null,"topics":["api","fastapi","fastapi-crud","python","rest"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tinkoffjournal.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2020-05-28T10:07:32.000Z","updated_at":"2023-11-17T18:36:46.000Z","dependencies_parsed_at":"2023-02-15T06:46:57.068Z","dependency_job_id":null,"html_url":"https://github.com/tinkoffjournal/freddie","commit_stats":{"total_commits":38,"total_committers":8,"mean_commits":4.75,"dds":"0.39473684210526316","last_synced_commit":"138d17fe0324ddb941810b7a9b103f56fd42c68c"},"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tinkoffjournal%2Ffreddie","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tinkoffjournal%2Ffreddie/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tinkoffjournal%2Ffreddie/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tinkoffjournal%2Ffreddie/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tinkoffjournal","download_url":"https://codeload.github.com/tinkoffjournal/freddie/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":234619327,"owners_count":18861455,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","fastapi","fastapi-crud","python","rest"],"created_at":"2024-08-03T23:01:02.780Z","updated_at":"2025-09-29T11:31:39.808Z","avatar_url":"https://github.com/tinkoffjournal.png","language":"Python","funding_links":[],"categories":["APIs"],"sub_categories":[],"readme":"# 🕺 Freddie\n\n[![pypi](https://img.shields.io/pypi/v/freddie)](https://pypi.org/project/freddie/)\n[![codecov](https://img.shields.io/codecov/c/github/tinkoffjournal/freddie)](https://codecov.io/gh/tinkoffjournal/freddie)\n\nDeclarative CRUD viewsets for [FastAPI](https://fastapi.tiangolo.com/) with optional database (Postgres) objects support.\nInspired by [Django REST Framework](https://www.django-rest-framework.org/).\n\n## Rationale\n\n_Freddie_ is aimed to solve some problems of building CRUD domain using FastAPI framework:\n1. Most logic is repetitive (run API some action on set of objects described in schema), while functional approach of routes\nin FastAPI does not assume easy reuse. That may lead to code duplication or complicated helper functions.\n2. API objects (Pydantic models) are validated both on request and response by default—which is good at some point—but a) may be the cause\nof performance loss with complex nested structures, b) useless if you store already validated data in persistent state.\n\n## Features\n\n* Predefined class-based viewsets for basic REST API operations\n* Schema-based serialization of response objects without running Pydantic model validation (can be optionally turned on)—with\nsaving auto-generated OpenAPI schema\n* Built-in mixins for pagination and GraphQL-alike schema fields retrieval\n* Optional tools for working with Postgres database objects in viewsets: use [Peewee](http://peewee-orm.com) for query building\nand [aiopg](https://aiopg.readthedocs.io/) for async database operations. Include fine-grained database queries based on\nschema and automatic joining/prefetching of related objects\n\n## Installation\n\nCore functionality only (basic viewsets \u0026 schema classes):\n\n```bash\npip install freddie\n```\n\nDatabase tools:\n\n```bash\npip install freddie[db]\n```\n\n## Usage\n\nLet's create a viewset for managing content posts. It will provide full kit of actions with simple mock data:\n\n```python\nfrom typing import List\nfrom fastapi import FastAPI\nfrom freddie.exceptions import NotFound\nfrom freddie.schemas import Schema\nfrom freddie.viewsets import FieldedViewset, PaginatedListViewset, route, ViewSet\n\n# Schema is just a subset of Pydantic model\nclass Post(Schema):\n    id: int = ...\n    title: str = ...\n    content: str = ''\n    metadata: dict = {}\n\n    class Config:\n        # By default, only post ID \u0026 title are returned in response.\n        # All other allowed fields are requested via ?fields=... query parameter\n        default_readable_fields = {'id', 'title'}\n\n\npost = Post(id=1, title='Freddie', content='Mercury', metadata={'views': 42})\n\n# All action methods (list, retrieve, create, update, destroy) must be implemented in this case.\n# Other combinations are also possible, as in DRF (see freddie.viewsets module)\nclass PostViewSet(\n    FieldedViewset,  # Allows retrieving non-default schema fields from query params\n    PaginatedListViewset,  # Adds paginator parameter with limit/offset query params\n    ViewSet\n):\n    schema = Post\n    list_schema = List[Post]  # custom list response schema\n    detail_schema = Post  # custom detail response schema\n\n    # Default viewset pagination options are set here\n    class Paginator:\n        default_limit = 10\n        max_limit = 100\n\n    # Async generators are supported as well\n    async def list(self, *, paginator, fields, request):\n        for i in range(1, paginator.limit + 1):\n            item_id = i + paginator.offset\n            yield Post(id=item_id, title=f'Freddie #{item_id}')\n\n    # Handler functions may be both sync \u0026 async\n    def retrieve(self, pk, *, fields, request):\n        if pk != post.id:\n            raise NotFound\n        return post\n\n    async def create(self, body, *, request):\n        return body\n\n    async def update(self, pk, body, *, request):\n        updated_post = post.copy()\n        for key, value in body.dict(exclude_unset=True).items():\n            setattr(updated_post, key, value)\n        return updated_post\n\n    async def destroy(self, pk, *, request):\n        ...\n\n    # Add custom handler on /meta path\n    @route(detail=False, summary='List metadata')\n    async def meta(self):\n        return post.metadata\n\n    # Add custom handler on /{pk}/retrieve_meta path\n    @route(detail=True, summary='Retrieve post metadata')\n    async def retrieve_meta(self, pk: int):\n        return {pk: post.metadata}\n\n\napp = FastAPI()\n# All viewsets are regular FastAPI routers\napp.include_router(PostViewSet(), prefix='/posts')\n```\n\nPseudocode extract from viewset OpenAPI schema:\n\n```\npaths: {\n  /posts/{pk}/meta: {\n    get: { tags: [\"Post\"], summary: \"Retrieve post metadata\", operationId: \"post_retrieve_meta\", parameters: [{…}] }\n  },\n  /posts/meta: {\n    get: { tags: [\"Post\"], summary: \"List metadata\", operationId: \"posts_meta\", responses: {…} }\n  },\n  /posts/{pk}: {\n    get: { tags: [\"Post\"], summary: \"Retrieve post\", operationId: \"get_post\", parameters: [{…}, {…}] },\n    put: { tags: [\"Post\"], summary: \"Full update post\", operationId: \"full_update_post\", parameters: [{…}] },\n    delete: { tags: [\"Post\"], summary: \"Delete post\", operationId: \"delete_post\", parameters: [{…}] },\n    patch: { tags: [\"Post\"], summary: \"Update post\", operationId: \"update_post\", parameters: [{…}] }\n  },\n  /posts/: {\n    get: { tags: [\"Post\"], summary: \"List posts\", operationId: \"list_posts\", parameters: [{…}, {…}, {…}] },\n    post: { tags: [\"Post\"], summary: \"Create post\", operationId: \"create_post\", requestBody: {…} }\n  }\n}\n```\n\n\u003cdetails markdown=\"1\"\u003e\n\u003csummary\u003eExample API requests \u0026 responses\u003c/summary\u003e\n\n`GET /posts/?limit=3\u0026offset=1 → 200 OK`\n```json\n[\n  {\n    \"title\": \"Freddie #2\",\n    \"id\": 2\n  },\n  {\n    \"title\": \"Freddie #3\",\n    \"id\": 3\n  },\n  {\n    \"title\": \"Freddie #4\",\n    \"id\": 4\n  }\n]\n```\n\n`GET /posts/1/?fields=content,metadata → 200 OK`\n```json\n{\n  \"title\": \"Freddie\",\n  \"id\": 1,\n  \"content\": \"Mercury\",\n  \"metadata\": {\n    \"views\": 42\n  }\n}\n```\n\n`POST /posts/ {\"id\": 2, \"title\": \"Another Freddie\"} → 201 CREATED`\n```json\n{\n  \"title\": \"Another Freddie\",\n  \"id\": 2,\n  \"content\": \"\",\n  \"metadata\": {}\n}\n```\n\n`PATCH /posts/1/ {\"content\": \"Broke free\"} → 200 OK`\n```json\n{\n  \"title\": \"Freddie\",\n  \"id\": 1,\n  \"content\": \"Broke free\",\n  \"metadata\": {\n    \"views\": 42\n  }\n}\n```\n\n`DELETE /posts/1/ → 204 NO CONTENT`\n\u003c/details\u003e\n\nThe real power of viewsets reveals in working with database. Let's store our posts in Postgres database and add relations with posts' authors and tags:\n\n```python\nfrom typing import List, Union\n\nfrom fastapi import FastAPI\nfrom freddie.db import Database, DatabaseManager\nfrom freddie.db.models import (\n    CharField, ForeignKeyField, ManyToManyField, Model as _Model, ThroughModel, depends_on, TextField, JSONField\n)\nfrom freddie.schemas import Schema\nfrom freddie.viewsets import FieldedViewset, FilterableListViewset, PaginatedListViewset, ModelViewSet\nfrom freddie.viewsets.signals import post_delete, post_save, signal\nfrom pydantic import constr\n\n# Database object stores connection options, while DatabaseManager runs async stuff\ndb = Database('freddie', user='freddie')\ndb_manager = DatabaseManager(db)\n\n\n# Model class is a subset of one used by Peewee ORM\nclass Model(_Model):\n    manager = db_manager\n\n    class Meta:\n        database = db\n\n\n# Let's first declare models for database\n\nclass Author(Model):\n    nickname = CharField(max_length=127, unique=True)\n    first_name = CharField(max_length=127)\n    last_name = CharField(max_length=127, default='')\n\n\nclass Tag(Model):\n    name = CharField(max_length=127)\n    slug = CharField(max_length=127, unique=True)\n\n\nclass Post(Model):\n    title = CharField(max_length=255)\n    slug = CharField(unique=True, max_length=63)\n    category = CharField(max_length=63)\n    content = TextField(default='')\n    metadata = JSONField(default=dict)\n    author = ForeignKeyField(Author, null=True)\n    # ManyToManyField is not a real DB column, but interface to object relations.\n    # The relations model is defined below\n    tags = ManyToManyField(Tag, 'PostTags')\n\n    @property\n    # This decorator declares DB fields that must be selected for API object property with the same name\n    @depends_on(slug)\n    def url(self):\n        return f'http://example.com/{self.slug}/'\n\n    @property\n    # In this case, the whole related Author object will be joined\n    @depends_on(author)\n    def author_name(self):\n        return f'{self.author.first_name} {self.author.last_name}'\n\n\nclass PostTags(Model, ThroughModel):\n    post = ForeignKeyField(Post, on_delete='CASCADE')\n    tag = ForeignKeyField(Tag, on_delete='CASCADE')\n\n\n# And now time for serialization schemas.\n# All text fields are defined as constraints with max. length (Pydantic's Field can also be used),\n# to avoid database errors on writing longer values.\n# If schema value is greater than in DB model (or not set), exception will be raised.\n\nclass AuthorSchema(Schema):\n    id: int\n    nickname: constr(max_length=127)\n    first_name: constr(max_length=127)\n    last_name: constr(max_length=127) = ''\n\n    class Config:\n        default_readable_fields = {'first_name', 'last_name'}\n\n\nclass TagSchema(Schema):\n    id: int\n    name: constr(max_length=127)\n    slug: constr(max_length=127)\n\n    class Config:\n        default_readable_fields = {'name'}\n\n\nclass PostSchema(Schema):\n    id: int\n    title: constr(max_length=255)\n    slug: constr(max_length=63)\n    category: constr(max_length=63)\n    url: str = None\n    content: str = ''\n    metadata: dict = {}\n    author: AuthorSchema = {}\n    author_id: int = None\n    tags: List[TagSchema] = []\n    tags_ids: List[int] = []\n\n    class Config:\n        default_readable_fields = {'id', 'title', 'url'}\n        read_only_fields = {'url', 'author', 'tags'}\n\n\nclass PostSchemaOnWrite(PostSchema):\n    id: int = None\n\n\nclass PostViewSet(\n    FieldedViewset, FilterableListViewset, PaginatedListViewset, ModelViewSet\n):\n    schema = PostSchema\n    write_schema = PostSchemaOnWrite\n    model = Post\n    # Post can be retrieved by auto ID or unique slug. So we need to set types for FastAPI \u0026 DB field to build query.\n    # If DB field is not unique, exception will be raised.\n    pk_type = Union[int, str]\n    secondary_lookup_field = Post.slug\n\n    @signal(post_save)\n    async def on_post_save(self, obj, obj_before_update=None, created=False, **params):\n        # Do some stuff in background after post is saved (on create or after update).\n        # obj is the current post state and obj_before_update is the state before action was run.\n        ...\n\n    @signal(post_delete)\n    async def on_post_delete(self, obj, **params):\n        # Do some stuff in background after post was deleted.\n        ...\n\n    class Filter:\n        # Enable simple filter by post category\n        category: str = None\n\n\napp = FastAPI()\napp.include_router(PostViewSet(), prefix='/posts')\n\n# Open DB connection on app start-up \u0026 close on shutdown\n@app.on_event('startup')\nasync def on_startup():\n    await db_manager.connect()\n\n@app.on_event('shutdown')\nasync def on_shutdown():\n    await db_manager.close()\n```\n\nSo what?\n\n* Now all API actions process objects in database\n* You can build effective queries that select only necessery fields from DB tables and join related models.\nE.g. `GET /posts/1?fields=content\u0026author(nickname)` will retrieve post default readable schema fields (ID, title and URL) + content field + joined author object,\nfor which we additionaly get non-default nickname field\n* Many-to-many relations in list action are prefetched automatically to avoid N+1 problem\n* Related objects are added via `*_id` (or `*_ids` for M2M relations) postfixed request body field on create/update\n* Post can be found either by ID (`GET /posts/1`) or by its unique slug (`GET /posts/intro`)\n* You can filter posts by fields declared in `Filter` config class: `GET /posts?category=longread`\n* You can add background tasks in declarative way to run after post was created, updated or deleted\n\n## TBD\n\n- [ ] Mixin class with dependency for sorting in list action\n- [ ] More advanced filter operators (not/in/lt/gt etc.)\n- [ ] Viewset's default response class schema description \u0026 inclusion into API schema (correct API client code generation when using enveloped responses)\n\n## Local development \u0026 Testing\n\n```bash\nmake dev \u0026\u0026 . venv/bin/activate\nmake test\n```\n\n## Why is it called _Freddie_?\n\nBecause the backend of [Tinkoff Journal](https://journal.tinkoff.ru/?utm_source=freddie) content API,\nwhich Freddie was originally developed for, is called _Mercury_.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftinkoffjournal%2Ffreddie","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftinkoffjournal%2Ffreddie","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftinkoffjournal%2Ffreddie/lists"}