MCPcopy Index your code
hub / github.com/danielhasan1/fastapi-listing

github.com/danielhasan1/fastapi-listing @v0.3.4

Chat with this repo
repository ↗ · DeepWiki ↗ · release v0.3.4 ↗ · + Follow
305 symbols 1,107 edges 55 files 37 documented · 12% updated 16mo agov0.3.4 · 2025-02-28★ 1396 open issues
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

fastapi-listing

Advanced items listing library that gives you freedom to design really complex listing APIs using component based architecture.

.github/workflows/deploy.yml .github/workflows/tests.yml PyPI - Programming Language codecov Downloads

Comes with: - pre defined filters - pre defined paginator - pre defined sorter

Advantage

  • simplify the intricate process of designing and developing complex listing APIs
  • Design components(USP) and plug them from anywhere
  • Components can be reusable
  • Best for fast changing needs

Installing

Using pip:

pip install fastapi-listing

Quick Example

Attaching example of it running against the mysql employee db

There are two ways to implement a listing API using fastapi listing

  • inline implementation
  • class based implementation

for both we will be needing a dao(data access object) class

First let's look at inline implementation.

# main.py

from fastapi import FastAPI
from pydantic import BaseModel, Field
from datetime import date

from sqlalchemy import Column, Date, Enum, Integer, String
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import Session

from fastapi_listing.paginator import ListingPage
from fastapi_listing import FastapiListing, MetaInfo
from fastapi_listing.dao import GenericDao


Base = declarative_base()
app = FastAPI()


class Employee(Base):
    __tablename__ = 'employees'

    emp_no = Column(Integer, primary_key=True)
    birth_date = Column(Date, nullable=False)
    first_name = Column(String(14), nullable=False)
    last_name = Column(String(16), nullable=False)
    gender = Column(Enum('M', 'F'), nullable=False)
    hire_date = Column(Date, nullable=False)

# Dao class
class EmployeeDao(GenericDao):
    """write your data layer access logic here. keep it raw!"""
    name = "employee"
    model = Employee # sqlalchemy model class (support for pymongo/tortoise orm is in progress)


class EmployeeListDetails(BaseModel):
    emp_no: int = Field(alias="empid", title="Employee ID")
    birth_date: date = Field(alias="bdt", title="Birth Date")
    first_name: str = Field(alias="fnm", title="First Name")
    last_name: str = Field(alias="lnm", title="Last Name")
    gender: str = Field(alias="gdr", title="Gender")
    hire_date: date = Field(alias="hdt", title="Hiring Date")

    class Config:
        orm_mode = True
        allow_population_by_field_name = True


@app.get("/employees", response_model=ListingPage[EmployeeListDetails])
def get_employees(db: Session):
    dao = EmployeeDao(read_db=db)
    # passing pydantic serializer is optional, automatically generates a
    # select query based on pydantic class fields for easy cases like columns of same table
    # if not passed then provide a select query in dao layer
    return FastapiListing(dao=dao, pydantic_serializer=EmployeeListDetails 
                          ).get_response(MetaInfo(default_srt_on="emp_no")) # by default sort in desc order
    # let's say pydantic class contains compute fields then pass custom_fields=True (by default False)
    return FastapiListing(dao=dao,
                          pydantic_serializer=EmployeeListDetails,
                          custom_fields=True # here setting custom field True to avoid unknown attributes error
                          ).get_response(MetaInfo(default_srt_on="emp_no"))

Voila 🎉 your very first listing response

Auto generated query doesn't fulfil your use case❓️

# Overwriting default read method in dao class
class EmployeeDao(GenericDao):
    """write your data layer access logic here. keep it raw!"""
    name = "employee"
    model = Employee

    def get_default_read(self, fields_to_read: Optional[list]):
        """
        Extend and return your query from here.
        Use it when use cases are comparatively easier than complex.
        Alternatively fastapi-listing provides a robust way to write performance packed queries 
        for complex APIs which we will look at later.
        """
        query = self._read_db.query(Employee)
        return query


@app.get("/employees", response_model=ListingPage[EmployeeListDetails])
def get_employees(db: Session):
    dao = EmployeeDao(read_db=db)
    # note we removed all optional named params here
    return FastapiListing(dao=dao).get_response(MetaInfo(default_srt_on="emp_no"))

Adding client site features

Django 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.

Add your custom adaptor class for reading filter/sorter/paginator client request params

Below is the default implementation. You will be writing your own adaptor definition

from typing import Literal
from fastapi_listing.service.adapters import CoreListingParamsAdapter
from fastapi_listing import utils

class YourAdapterClass(CoreListingParamsAdapter): # Extend to add your behaviour
    """Utilise this adapter class to make your remote client site:
    - filter,
    - sorter,
    - paginator.
    query params adapt to fastapi listing library.
    With this you can utilise same listing api to multiple remote client
    even if it's a front end server or other backend server.

    fastapi listing is always going to request one of the following fundamental key if you want to use it
    - sort
    - filter
    - pagination

    supported formats for
    filter:
    simple filter - [{"field":"<key used in filter mapper>", "value":{"search":"<client param>"}}, ...]
    if you are using a range filter -
    [{"field":"<key used in filter mapper>", "value":{"start":"<start range>", "end": "<end range>"}}, ...]
    if you are using a list filter i.e. search on given items
    [{"field":"<key used in filter mapper>", "value":{"list":["<client params>"]}}, ...]

    sort:
    [{"field":<"key used in sort mapper>", "type":"asc or "dsc"}, ...]
    by default single sort allowed you can change it by extending sort interceptor

    pagination:
    {"pageSize": <integer page size>, "page": <integer page number 1 based>}
    """

    def get(self, key: Literal["sort", "filter", "pagination"]):
        """
        @param key: Literal["sort", "filter", "pagination"]
        @return: List[Optional[dict]] for filter/sort and dict for paginator
        """
        return utils.dictify_query_params(self.dependency.get(key))

Once your adaptor class is set

Adding filter feature

➡️ lets add filters on Employee for: 1. gender - return only Employees belonging to 'X' gender where X could be anything. 2. DOB - return Employees belonging to a specific range of DOB. 3. First Name - return Employees only starting with specific first names.

from fastapi import Request
from sqlalchemy.orm import Session

from fastapi_listing.paginator import ListingPage
from fastapi_listing.filters import generic_filters # collection of inbuilt filters
from fastapi_listing.factory import filter_factory # register filter against a listing
from fastapi_listing import MetaInfo, FastapiListing


emp_filter_mapper = {
    "gdr": ("Employee.gender", generic_filters.EqualityFilter),
    "bdt": ("Employee.birth_date", generic_filters.MySqlNativeDateFormateRangeFilter),
    "fnm": ("Employee.first_name", generic_filters.StringStartsWithFilter),
}
filter_factory.register_filter_mapper(emp_filter_mapper)


@app.get("/employees", response_model=ListingPage[EmployeeListDetails])
def get_employees(request: Request, db: Session):
    dao = EmployeeDao(read_db=db)
    return FastapiListing(request=request, dao=dao).get_response(
        MetaInfo(default_srt_on="emp_no",
                 filter_mapper=emp_filter_mapper,
                 feature_params_adapter=YourAdapterClass))

    # or you dont wanna pass request?
    # extract required data from reqeust and pass it directly 
    params = request.query_params
    filter_, sort_, pagination = params.get("filter"), params.get("sort"), params.get("paginator")

    dao = EmployeeDao(read_db=db)
    return FastapiListing(dao=dao).get_response(
        MetaInfo(default_srt_on="emp_no",
                 filter_mapper=emp_filter_mapper,
                 feature_params_adapter=YourAdapterClass,
                 filter=filter_,
                 sort=sort_,
                 paginator=pagination))

Let's break it down

Filter mapper - a collection of allowed filters on your listing API. Any request outside of this mapper scope will not be executed for filtering safeguarding you from creepy API users.

generic_filters a collection of inbuilt filters supported by sqlalchemy orm A dictionary is defined with structure:

{"alias": tuple("sqlalchemy_model.field", filter_implementation)}

alias - A string used by client in case if you wanna avoid actual column names to client site.

tuple - will contain two items field name and filter implementation

from fastapi_listing.filters import generic_filters


emp_filter_mapper = {
    "gdr": ("Employee.gender", generic_filters.EqualityFilter),
    "bdt": ("Employee.birth_date", generic_filters.MySqlNativeDateFormateRangeFilter),
    "fnm": ("Employee.first_name", generic_filters.StringStartsWithFilter),
}

Register the above mapper with filter factory.

from fastapi_listing.factory import filter_factory


filter_factory.register_filter_mapper(emp_filter_mapper) # Register in global space or module level.

A client could request you like v1/employees?filter=[{"gdr":"M"}]

parse 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 access self.request directly there.

Assuming everything goes right above will produce a response with items filtered on gender field matching rows with 'M'

Sort Mapper - a collection of allowed sort on listing any request outside of this mapper scope will not be permitted for sort.

Simply define a dictionary with structure {"alias": "field"} if sorting on same column them omit model name & if sorting on a joined table column then add sqlalchemy class name like we did for filter {"alias":"sqlalchemy_model.field"}

listing_sort_mapper = {
        "code": "emp_no"
    }
return FastapiListing(dao=dao).get_response(
        MetaInfo(default_srt_on="emp_no",
                 filter_mapper=emp_filter_mapper,
                 sort_mapper=listing_sort_mapper,
                 feature_params_adapter=YourAdapterClass,
                 filter=filter_,
                 sort=sort_,
                 paginator=pagination))

# OR if passing request obj
return FastapiListing(request=request, dao=dao).get_response(
        MetaInfo(default_srt_on="emp_no",
                 filter_mapper=emp_filter_mapper,
                 sort_mapper=listing_sort_mapper,
                 feature_params_adapter=YourAdapterClass))

A client could request you like v1/employees?sort={"code":<some_code:int>} or followed by filter v1/employees?filter=[{"gdr":"M"}]&sort={"code":<some_code:int>, "type":"asc"} and the response should contain list items sorted by employee code column in ascending order.

Note we didn't registered sort mapper like we did for filter mapper.

Similarly, for paginator v1/employees?pagination={"page":1, "pageSize":10} or followed by filter and sort v1/employees?filter=[{"gdr":"M"}]&sort={"code":<some_code:int>, "type":"asc"}&pagination={"page":1, "pageSize":10}

Above will produce listing page of items 10 or dynamically client could change page size.

One 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 if you want to increase/decrease this default limit then simply pass the limit in MetaInfo

You can also change the default page size from 10 to anything you would want

return FastapiListing(request=request, dao=dao).get_response(
        MetaInfo(default_srt_on="emp_no",
                 filter_mapper=emp_filter_mapper,
                 sort_mapper=listing_sort_mapper,
                 max_page_size=25, # here change max page size
                 default_page_size=10, # here change default page size
                 feature_params_adapter=YourAdapterClass))

Class Based implementation

Quick Example to convey the context

```python from fastapi import FastAPI

from sqlalchemy import Column, Date, String, ForeignKey from sqlalchemy.orm import declarative_base from sqlalchemy.orm import Session, relationship from fastapi_listing import ListingService, FastapiListing from fastapi_listing.filters import generic_filters from fastapi_listing import loader from fastapi_listing.paginator import ListingPage

Base = declarative_base() app = FastAPI()

class Title(Base): tablename = 'titles'

emp_no = Column(ForeignKey('employees.emp_no', ondelete='CASCADE'), primary_key=True, nullable=False)
title = Column(String(50), primary_key=True, nullabl

Core symbols most depended-on inside this repo

get
called by 74
fastapi_listing/service/adapters.py
extract_field
called by 16
fastapi_listing/filters/generic_filters.py
filter
called by 14
fastapi_listing/filters/generic_filters.py
register_dao
called by 10
fastapi_listing/dao/dao_registry.py
register_strategy
called by 9
fastapi_listing/factory/strategy.py
create
called by 9
tests/dao_setup.py
create
called by 8
fastapi_listing/abstracts/dao.py
register_interceptor
called by 7
fastapi_listing/factory/interceptor.py

Shape

Method 150
Class 98
Function 50
Route 7

Languages

Python100%

Modules by API surface

fastapi_listing/filters/generic_filters.py36 symbols
tests/test_main_v2.py23 symbols
tests/dao_setup.py23 symbols
fastapi_listing/abstracts/listing.py23 symbols
tests/pydantic_setup.py15 symbols
fastapi_listing/paginator/page_builder.py14 symbols
fastapi_listing/interface/listing_meta_info.py14 symbols
fastapi_listing/service/_core_listing_service.py13 symbols
tests/service_setup.py12 symbols
fastapi_listing/errors.py10 symbols
tests/test_main.py9 symbols
fastapi_listing/middlewares.py8 symbols

Datastores touched

(mysql)Database · 1 repos
employeesDatabase · 1 repos

For agents

$ claude mcp add fastapi-listing \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page