Skip to main content

Advaned Data Listing Library for FastAPI

Project description

fastapi-listing

Advanced items listing library that gives you freedom to design complex listing APIs that can be read by human.

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

➡️ Craft powerful Listing REST APIs designed to serve websites akin to Stack Overflow:

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

Usage

➡️ With the New Compact Version(Older version was provided with a guide style which is supported by this version as well) create your listing API in 2 easy steps 🥳

1️⃣ Create a Dao (Data Access Object) layer (a simple class)

from fastapi_listing.dao import GenericDao
from app.model import Employee


# your dao (data access object) placed here for the sake of example
class EmployeeDao(GenericDao):
    """write your data layer access logic here. keep it raw!"""
    name = "employee"
    model = Employee # your sqlalchemy model, support for more orm is coming soon

2️⃣ Just call FastapiListing

from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session

# import fastapi-listing dependencies
from fastapi_listing.paginator import ListingPage # default pydantic page model, you can create/extend your own
from fastapi_listing import FastapiListing, MetaInfo

app = FastAPI()  # create FastAPI app

def get_db() -> Session:
    """
    replicating sessionmaker for any fastapi app.
    anyone could be using a different way or opensource packages to produce sessions.
    it all comes down to a single result that is yielding a session.
    for the sake of simplicity and testing purpose I'm replicating this behaviour in this way.
    :return: Session
    """



# your pydantic response class if you are using one
# Supports pydantic v2
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=Depends(get_db)):
    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
    return FastapiListing(dao=dao, pydantic_serializer=EmployeeListDetails
                          ).get_response(MetaInfo(default_srt_on="emp_no"))    

Voila 🎉 your very first listing response(that's even extensible user can manipulate default page structure)

Your pydantic class contains some dynamic fields that you populate at runtime❓️

@app.get("/employees", response_model=ListingPage[EmployeeListDetails])
def get_employees(db=Depends(get_db)):
    dao = EmployeeDao(read_db=db)
    return FastapiListing(dao=dao,
                          pydantic_serializer=EmployeeListDetails, # optional
                          custom_fields=True # just tell fastapi-listing that your model contains custom_fields
                          ).get_response(MetaInfo(default_srt_on="emp_no"))

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

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=Depends(get_db)):
    dao = EmployeeDao(read_db=db)
    return FastapiListing(dao=dao).get_response(MetaInfo(default_srt_on="emp_no"))

Thinking about adding filters???

Don't worry I've got you covered😎

➡️ Say you want to add filter 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_listing.filters import generic_filters # collection of inbuilt filters
from fastapi_listing.factory import filter_factory # import filter_factory to register filter against a listing

# {"alias": ("<model.field>", "<filter_definition>")}
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) # You just registered the number of filters allowed to client on this listing


@app.get("/employees", response_model=ListingPage[EmployeeListDetails])
def get_employees(request: Request, db=Depends(get_db)):
    """
    request is optional to pass.
    you can pass filter query_param or use request object.
    make fastapi-listing adapt to your client existing query_param format
    """
    dao = EmployeeDao(read_db=db)
    return FastapiListing(request=request, dao=dao).get_response(
        MetaInfo(default_srt_on="emp_no", filter_mapper=emp_filter_mapper))
    

Thinking about how fastapi-listing reads filter/sorter/paginator params❓️

# Extend adapter to make fastapi-listing adapt your existing clients

# default implementation

from fastapi_listing.service.adapters import CoreListingParamsAdapter

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.

    core service is always going to request one of the following fundamental key
    - sort
    - filter
    - pagination
    depending upon this return the appropriate transformed client param back to fastapi listing
    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))

Pass request or extract feature(filter/sorter/paginator) params at router and pass them as kwargs

@app.get("/employees", response_model=ListingPage[EmployeeListDetails])
def get_employees(request: Request, db=Depends(get_db)):
    params = request.query_params
    # filter, sort. pagination = params.get("filter"), params.get("sort"), params.get("paginator")
    # you can pass above args as kwargs in MetaInfo
    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))
    

Check out docs for supported list of filters. Additionally, you can create custom filters as well.

Thinking about adding Sorting???

I won't leave you hanging there as well😎

@app.get("/employees", response_model=ListingPage[EmployeeListDetails])
def get_employees(request: Request, db=Depends(get_db)):
    # define it here or anywhere
    emp_sort_mapper = {
        "cd": "Employee.emp_no",
        "bdt": "Employee.birth_date"
    }
    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,
                 sort_mapper=emp_sort_mapper))

Provided features are not meeting your requirements???

It is customizable.😎

➡️ You can write custom:

  • Query
  • Filter
  • Sorter
  • Paginator

You can check out customisation section in docs after going through basics and tutorials.

Check out my other repo to see some examples

Features and Readability hand in hand 🤝

  • Easy-to-use API for listing and formatting data
  • Built-in support for pagination, sorting and filtering
  • Well defined interface for filter, sorter, paginator
  • Support Dependency Injection for easy testing
  • Room to adapt the existing remote client query param semantics
  • Write standardise listing APIs that will be understood by generations of upcoming developers
  • Write listing features which is easy on human mind to extend or understand
  • Break down the most complex listing data APIs into digestible piece of code

With FastAPI Listing you won't end up like

Documentation

View full documentation at: https://fastapi-listing.readthedocs.io ███▓▓░️░️░35%️░️░️░️

Feedback, Questions?

Any form of feedback and questions are welcome! Please create an issue 💭 here.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fastapi-listing-0.3.2.tar.gz (40.8 kB view hashes)

Uploaded Source

Built Distribution

fastapi_listing-0.3.2-py3-none-any.whl (52.0 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page