{"id":15020962,"url":"https://github.com/danielhasan1/fastapi-listing","last_synced_at":"2025-04-07T07:18:07.629Z","repository":{"id":162258721,"uuid":"618357670","full_name":"danielhasan1/fastapi-listing","owner":"danielhasan1","description":"Advanced items listing library that gives you freedom to design complex listing REST APIs that can be read by human.","archived":false,"fork":false,"pushed_at":"2025-02-28T13:55:11.000Z","size":33417,"stargazers_count":53,"open_issues_count":5,"forks_count":3,"subscribers_count":3,"default_branch":"dev","last_synced_at":"2025-03-31T06:08:55.185Z","etag":null,"topics":["fastapi","json","library","listing-api","python","rest-api","web"],"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/danielhasan1.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,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-03-24T09:47:15.000Z","updated_at":"2025-02-28T13:51:05.000Z","dependencies_parsed_at":"2024-01-30T11:28:48.299Z","dependency_job_id":"e300d057-446e-45a7-acd9-8b1a5c84bfb2","html_url":"https://github.com/danielhasan1/fastapi-listing","commit_stats":{"total_commits":135,"total_committers":2,"mean_commits":67.5,"dds":"0.24444444444444446","last_synced_commit":"8981e4a5b07f34f99f5cc4c397e45314dc069d28"},"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielhasan1%2Ffastapi-listing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielhasan1%2Ffastapi-listing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielhasan1%2Ffastapi-listing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielhasan1%2Ffastapi-listing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/danielhasan1","download_url":"https://codeload.github.com/danielhasan1/fastapi-listing/tar.gz/refs/heads/dev","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247608160,"owners_count":20965953,"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":["fastapi","json","library","listing-api","python","rest-api","web"],"created_at":"2024-09-24T19:55:56.912Z","updated_at":"2025-04-07T07:18:07.599Z","avatar_url":"https://github.com/danielhasan1.png","language":"Python","readme":"# fastapi-listing\n\nAdvanced items listing library that gives you freedom to design really complex listing APIs using component based architecture.\n\n[![.github/workflows/deploy.yml](https://github.com/danielhasan1/fastapi-listing/actions/workflows/deploy.yml/badge.svg)](https://github.com/danielhasan1/fastapi-listing/actions/workflows/deploy.yml)\n[![.github/workflows/tests.yml](https://github.com/danielhasan1/fastapi-listing/actions/workflows/tests.yml/badge.svg)](https://github.com/danielhasan1/fastapi-listing/actions/workflows/tests.yml) ![PyPI - Programming Language](https://img.shields.io/pypi/pyversions/fastapi-listing.svg?color=%2334D058)\n[![codecov](https://codecov.io/gh/danielhasan1/fastapi-listing/branch/dev/graph/badge.svg?token=U29ZRNAH8I)](https://codecov.io/gh/danielhasan1/fastapi-listing) [![Downloads](https://static.pepy.tech/badge/fastapi-listing)](https://pepy.tech/project/fastapi-listing)\n\nComes with:\n- pre defined filters\n- pre defined paginator\n- pre defined sorter\n\n## Advantage\n- simplify the intricate process of designing and developing complex listing APIs\n- Design components(USP) and plug them from anywhere\n- Components can be **reusable**\n- Best for fast changing needs\n\n## Installing\n\nUsing [pip](https://pip.pypa.io/):\n\n```python\npip install fastapi-listing\n```\n\n## Quick Example\n\nAttaching example of it running against the [mysql employee db](https://dev.mysql.com/doc/employee/en/) \n\nThere are two ways to implement a listing API using fastapi listing\n\n- inline implementation\n- class based implementation\n\nfor both we will be needing a dao(data access object) class\n\n### First let's look at inline implementation.\n\n```python\n# main.py\n\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel, Field\nfrom datetime import date\n\nfrom sqlalchemy import Column, Date, Enum, Integer, String\nfrom sqlalchemy.orm import declarative_base\nfrom sqlalchemy.orm import Session\n\nfrom fastapi_listing.paginator import ListingPage\nfrom fastapi_listing import FastapiListing, MetaInfo\nfrom fastapi_listing.dao import GenericDao\n\n\nBase = declarative_base()\napp = FastAPI()\n\n\nclass Employee(Base):\n    __tablename__ = 'employees'\n\n    emp_no = Column(Integer, primary_key=True)\n    birth_date = Column(Date, nullable=False)\n    first_name = Column(String(14), nullable=False)\n    last_name = Column(String(16), nullable=False)\n    gender = Column(Enum('M', 'F'), nullable=False)\n    hire_date = Column(Date, nullable=False)\n\n# Dao class\nclass EmployeeDao(GenericDao):\n    \"\"\"write your data layer access logic here. keep it raw!\"\"\"\n    name = \"employee\"\n    model = Employee # sqlalchemy model class (support for pymongo/tortoise orm is in progress)\n\n\nclass EmployeeListDetails(BaseModel):\n    emp_no: int = Field(alias=\"empid\", title=\"Employee ID\")\n    birth_date: date = Field(alias=\"bdt\", title=\"Birth Date\")\n    first_name: str = Field(alias=\"fnm\", title=\"First Name\")\n    last_name: str = Field(alias=\"lnm\", title=\"Last Name\")\n    gender: str = Field(alias=\"gdr\", title=\"Gender\")\n    hire_date: date = Field(alias=\"hdt\", title=\"Hiring Date\")\n\n    class Config:\n        orm_mode = True\n        allow_population_by_field_name = True\n    \n\n@app.get(\"/employees\", response_model=ListingPage[EmployeeListDetails])\ndef get_employees(db: Session):\n    dao = EmployeeDao(read_db=db)\n    # passing pydantic serializer is optional, automatically generates a\n    # select query based on pydantic class fields for easy cases like columns of same table\n    # if not passed then provide a select query in dao layer\n    return FastapiListing(dao=dao, pydantic_serializer=EmployeeListDetails \n                          ).get_response(MetaInfo(default_srt_on=\"emp_no\")) # by default sort in desc order\n    # let's say pydantic class contains compute fields then pass custom_fields=True (by default False)\n    return FastapiListing(dao=dao,\n                          pydantic_serializer=EmployeeListDetails,\n                          custom_fields=True # here setting custom field True to avoid unknown attributes error\n                          ).get_response(MetaInfo(default_srt_on=\"emp_no\"))\n```\n\nVoila 🎉 your very first listing response\n\n![](https://drive.google.com/uc?export=view\u0026id=1amgrAdGP7WvXfiNlCYJZPC9fz4_1CidE)\n\n\nAuto generated query  doesn't fulfil your use case❓️\n\n```python\n# Overwriting default read method in dao class\nclass EmployeeDao(GenericDao):\n    \"\"\"write your data layer access logic here. keep it raw!\"\"\"\n    name = \"employee\"\n    model = Employee\n    \n    def get_default_read(self, fields_to_read: Optional[list]):\n        \"\"\"\n        Extend and return your query from here.\n        Use it when use cases are comparatively easier than complex.\n        Alternatively fastapi-listing provides a robust way to write performance packed queries \n        for complex APIs which we will look at later.\n        \"\"\"\n        query = self._read_db.query(Employee)\n        return query\n\n\n@app.get(\"/employees\", response_model=ListingPage[EmployeeListDetails])\ndef get_employees(db: Session):\n    dao = EmployeeDao(read_db=db)\n    # note we removed all optional named params here\n    return FastapiListing(dao=dao).get_response(MetaInfo(default_srt_on=\"emp_no\"))\n```\n\n\n# Adding client site features\n\nDjango admin users gonna love filter feature. But before that lets do a little setup which no once can avoid to support a broad spectrum of clients unless you use native query param format which I doubt.\n\n## Add your custom adaptor class for reading filter/sorter/paginator client request params\n\nBelow is the default implementation. You will be writing your own adaptor definition\n\n```python\nfrom typing import Literal\nfrom fastapi_listing.service.adapters import CoreListingParamsAdapter\nfrom fastapi_listing import utils\n\nclass YourAdapterClass(CoreListingParamsAdapter): # Extend to add your behaviour\n    \"\"\"Utilise this adapter class to make your remote client site:\n    - filter,\n    - sorter,\n    - paginator.\n    query params adapt to fastapi listing library.\n    With this you can utilise same listing api to multiple remote client\n    even if it's a front end server or other backend server.\n\n    fastapi listing is always going to request one of the following fundamental key if you want to use it\n    - sort\n    - filter\n    - pagination\n\n    supported formats for\n    filter:\n    simple filter - [{\"field\":\"\u003ckey used in filter mapper\u003e\", \"value\":{\"search\":\"\u003cclient param\u003e\"}}, ...]\n    if you are using a range filter -\n    [{\"field\":\"\u003ckey used in filter mapper\u003e\", \"value\":{\"start\":\"\u003cstart range\u003e\", \"end\": \"\u003cend range\u003e\"}}, ...]\n    if you are using a list filter i.e. search on given items\n    [{\"field\":\"\u003ckey used in filter mapper\u003e\", \"value\":{\"list\":[\"\u003cclient params\u003e\"]}}, ...]\n\n    sort:\n    [{\"field\":\u003c\"key used in sort mapper\u003e\", \"type\":\"asc or \"dsc\"}, ...]\n    by default single sort allowed you can change it by extending sort interceptor\n\n    pagination:\n    {\"pageSize\": \u003cinteger page size\u003e, \"page\": \u003cinteger page number 1 based\u003e}\n    \"\"\"\n    \n    def get(self, key: Literal[\"sort\", \"filter\", \"pagination\"]):\n        \"\"\"\n        @param key: Literal[\"sort\", \"filter\", \"pagination\"]\n        @return: List[Optional[dict]] for filter/sort and dict for paginator\n        \"\"\"\n        return utils.dictify_query_params(self.dependency.get(key))\n\n```\n### Once your adaptor class is set\n \n## Adding filter feature\n\n➡️ lets add filters on Employee for:\n1.  gender - return only **Employees** belonging to 'X' gender where X could be anything.\n2.  DOB - return **Employees** belonging to a specific range of DOB.\n3.  First Name - return **Employees** only starting with specific first names.\n```python\nfrom fastapi import Request\nfrom sqlalchemy.orm import Session\n\nfrom fastapi_listing.paginator import ListingPage\nfrom fastapi_listing.filters import generic_filters # collection of inbuilt filters\nfrom fastapi_listing.factory import filter_factory # register filter against a listing\nfrom fastapi_listing import MetaInfo, FastapiListing\n\n\nemp_filter_mapper = {\n    \"gdr\": (\"Employee.gender\", generic_filters.EqualityFilter),\n    \"bdt\": (\"Employee.birth_date\", generic_filters.MySqlNativeDateFormateRangeFilter),\n    \"fnm\": (\"Employee.first_name\", generic_filters.StringStartsWithFilter),\n}\nfilter_factory.register_filter_mapper(emp_filter_mapper)\n\n\n@app.get(\"/employees\", response_model=ListingPage[EmployeeListDetails])\ndef get_employees(request: Request, db: Session):\n    dao = EmployeeDao(read_db=db)\n    return FastapiListing(request=request, dao=dao).get_response(\n        MetaInfo(default_srt_on=\"emp_no\",\n                 filter_mapper=emp_filter_mapper,\n                 feature_params_adapter=YourAdapterClass))\n    \n    # or you dont wanna pass request?\n    # extract required data from reqeust and pass it directly \n    params = request.query_params\n    filter_, sort_, pagination = params.get(\"filter\"), params.get(\"sort\"), params.get(\"paginator\")\n    \n    dao = EmployeeDao(read_db=db)\n    return FastapiListing(dao=dao).get_response(\n        MetaInfo(default_srt_on=\"emp_no\",\n                 filter_mapper=emp_filter_mapper,\n                 feature_params_adapter=YourAdapterClass,\n                 filter=filter_,\n                 sort=sort_,\n                 paginator=pagination))\n    \n```\n\n### Let's break it down\n\n**Filter mapper** - a collection of allowed filters on your listing API. Any request outside of this mapper scope\nwill not be executed for filtering safeguarding you from creepy API users.\n\n`generic_filters` a collection of inbuilt filters supported by sqlalchemy orm\nA dictionary is defined with structure:\n\n`{\"alias\": tuple(\"sqlalchemy_model.field\", filter_implementation)}`\n\n`alias` - A string used by client in case if you wanna avoid actual column names to client site.\n\n`tuple` - will contain two items field name and filter implementation\n\n```python\nfrom fastapi_listing.filters import generic_filters\n\n\nemp_filter_mapper = {\n    \"gdr\": (\"Employee.gender\", generic_filters.EqualityFilter),\n    \"bdt\": (\"Employee.birth_date\", generic_filters.MySqlNativeDateFormateRangeFilter),\n    \"fnm\": (\"Employee.first_name\", generic_filters.StringStartsWithFilter),\n}\n```\n\nRegister the above mapper with filter factory. \n\n```python\nfrom fastapi_listing.factory import filter_factory\n\n\nfilter_factory.register_filter_mapper(emp_filter_mapper) # Register in global space or module level.\n```\n\nA client could request you like `v1/employees?filter=[{\"gdr\":\"M\"}]`\n\nparse the above query_param in your adapter class like `[{\"field\":\"gdr\", \"value\":{\"search\":\"M\"}}]` if passed externally as kwarg then access it via `self.extra_context` in your adapter class or if passed request then\naccess `self.request` directly there.\n\nAssuming everything goes right above will produce a response with items filtered on gender field matching rows with 'M'\n\n**Sort Mapper** - a collection of allowed sort on listing any request outside of this mapper scope will\nnot be permitted for sort.\n\nSimply define a dictionary with structure `{\"alias\": \"field\"}` if sorting on same column them omit model name \u0026\nif sorting on a joined table column then add sqlalchemy class name like we did for filter `{\"alias\":\"sqlalchemy_model.field\"}`\n\n```python\nlisting_sort_mapper = {\n        \"code\": \"emp_no\"\n    }\nreturn FastapiListing(dao=dao).get_response(\n        MetaInfo(default_srt_on=\"emp_no\",\n                 filter_mapper=emp_filter_mapper,\n                 sort_mapper=listing_sort_mapper,\n                 feature_params_adapter=YourAdapterClass,\n                 filter=filter_,\n                 sort=sort_,\n                 paginator=pagination))\n\n# OR if passing request obj\nreturn FastapiListing(request=request, dao=dao).get_response(\n        MetaInfo(default_srt_on=\"emp_no\",\n                 filter_mapper=emp_filter_mapper,\n                 sort_mapper=listing_sort_mapper,\n                 feature_params_adapter=YourAdapterClass))\n```\n\nA client could request you like `v1/employees?sort={\"code\":\u003csome_code:int\u003e}` or followed by filter `v1/employees?filter=[{\"gdr\":\"M\"}]\u0026sort={\"code\":\u003csome_code:int\u003e, \"type\":\"asc\"}` \nand the response should contain list items sorted by employee code column in ascending order.\n\n**Note** we didn't registered sort mapper like we did for filter mapper.\n\nSimilarly, for paginator `v1/employees?pagination={\"page\":1, \"pageSize\":10}` or followed by filter and sort `v1/employees?filter=[{\"gdr\":\"M\"}]\u0026sort={\"code\":\u003csome_code:int\u003e, \"type\":\"asc\"}\u0026pagination={\"page\":1, \"pageSize\":10}`\n\nAbove will produce listing page of items 10 or dynamically client could change page size.\n\nOne thing to **Note** here is fastapi listing by default limits the client to reuqest maximum of 50 items at a time to safeguard your database \nif you want to increase/decrease this default limit then simply pass the limit in `MetaInfo`\n\n**You can also change the default page size from 10 to anything you would want**\n\n```python\nreturn FastapiListing(request=request, dao=dao).get_response(\n        MetaInfo(default_srt_on=\"emp_no\",\n                 filter_mapper=emp_filter_mapper,\n                 sort_mapper=listing_sort_mapper,\n                 max_page_size=25, # here change max page size\n                 default_page_size=10, # here change default page size\n                 feature_params_adapter=YourAdapterClass))\n```\n\n### Class Based implementation\nQuick Example to convey the context\n\n```python\nfrom fastapi import FastAPI\n\nfrom sqlalchemy import Column, Date, String, ForeignKey\nfrom sqlalchemy.orm import declarative_base\nfrom sqlalchemy.orm import Session, relationship\nfrom fastapi_listing import ListingService, FastapiListing\nfrom fastapi_listing.filters import generic_filters\nfrom fastapi_listing import loader\nfrom fastapi_listing.paginator import ListingPage\n\nBase = declarative_base()\napp = FastAPI()\n\nclass Title(Base):\n    __tablename__ = 'titles'\n\n    emp_no = Column(ForeignKey('employees.emp_no', ondelete='CASCADE'), primary_key=True, nullable=False)\n    title = Column(String(50), primary_key=True, nullable=False)\n    from_date = Column(Date, primary_key=True, nullable=False)\n    to_date = Column(Date)\n\n    employee = relationship('Employee')\n\n    \nclass EmployeeDao(GenericDao):\n    name = \"employee\"\n    model = Employee\n    \nclass TitleDao(GenericDao):\n    name = \"title\"\n    model = Title\n    \n@loader.register()\nclass EmployeeListingService(ListingService):\n    \"\"\"Class based listing API implementation\"\"\"\n    filter_mapper = {\n        \"gdr\": (\"Employee.gender\", generic_filters.EqualityFilter),\n        \"bdt\": (\"Employee.birth_date\", generic_filters.MySqlNativeDateFormateRangeFilter),\n        \"fnm\": (\"Employee.first_name\", generic_filters.StringStartsWithFilter),\n        \"lnm\": (\"Employee.last_name\", generic_filters.StringEndsWithFilter),\n        # below feature will require customisation to work at query level\n        \"desg\": (\"Employee.Title.title\", generic_filters.StringLikeFilter, lambda x: getattr(Title, x)) # registering filter with joined table field\n    }\n\n    sort_mapper = {\n        \"cd\": \"emp_no\"\n    }\n    default_srt_on = \"Employee.emp_no\"\n    default_dao = EmployeeDao\n\n    def get_listing(self):\n        # similar to above inline but instead of passing meta info uncompressed we pass self\n        # rest is handled implicityly like filter register\n        # one advantage here is every expect is validated so you get error when running server\n        resp = FastapiListing(self.request, self.dao, pydantic_serializer=EmployeeListDetails).get_response(self.MetaInfo(self))\n        return resp\n\n    \n@app.get(\"/employees\", response_model=ListingPage[EmployeeListDetails])\ndef get_employees(db: Session):\n    return EmployeeListingService(read_db=db).get_listing()\n```\n\nCheck out [docs](https://fastapi-listing.readthedocs.io/en/latest/tutorials.html#adding-filters-to-your-listing-api) for supported list of filters.\nAdditionally, you can create **custom filters** as well.\n\n## Provided features are not meeting your requirements???\n\nThe Applications are endless with customisations\n\n➡️ You can write custom:\n\n* Query\n* Filter\n* Sorter\n* Paginator\n\nYou can check out customisation section in docs after going through basics and tutorials.\n\nCheck out my other [repo](https://github.com/danielhasan1/test-fastapi-listing/blob/master/app/router/router.py) to see some examples\n\n## Features and Readability hand in hand 🤝\n\n - Well defined interface for filter, sorter, paginator\n - Support Dependency Injection for easy testing\n - Room to adapt the existing remote client query param semantics\n - Write standardise listing APIs that will be understood by generations of upcoming developers\n - Write listing features which is easy on human mind to extend or understand\n - Break down the most complex listing data APIs into digestible piece of code \n\nWhy readability and code quality matters in one picture...\n\n\u003cimg src=\"https://drive.google.com/uc?export=view\u0026id=1C2ZHltxpdyq4YmBsnbOu4HF9JGt6uMfQ\" width=\"600\" height=\"600\"/\u003e\n\n# Documentation\nView full documentation at: https://fastapi-listing.readthedocs.io (A work in progress)\n\n\n\n# Feedback, Questions?\n\nAny form of feedback and questions are welcome! Please create an issue  💭\n[here](https://github.com/danielhasan1/fastapi-listing/issues/new).\n","funding_links":[],"categories":["Third-Party Extensions"],"sub_categories":["Utils"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanielhasan1%2Ffastapi-listing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdanielhasan1%2Ffastapi-listing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanielhasan1%2Ffastapi-listing/lists"}