bk-fastapi-core 1.0.12
Fast-API based core functionality for bKash middlwares.
Requires Python: <4.0,>3.11

## FastAPI Core

### Features

* Scylla DB support
* Redis Support
* Firestore support
* Exceptions classes and handlers

### Installation

* Add the download repository to `pyproject.toml` or run the following command

```shell
poetry source add artifacthub_download https://artifacthub.bkash.com/repository/python-group/simple
```

* Now install the package

```shell
poetry add bk-fastapi-core==1.0.0 --source artifacthub_download --no-cache # you can change the version
```

### How to connect to db(scylla), Redis?

* At you lifespan([FastAPI Lifespan](https://fastapi.tiangolo.com/advanced/events/)), you need to connect the database.
  See the following example,

```python
@asynccontextmanager
async def app_lifespan(app: FastAPI):
    scylla_manager = ScyllaDBConnectionManager(
        db_hosts=[
            "172.18.0.2",
            "172.18.0.3",
            "172.18.0.4",
            "172.18.0.5",
            "172.18.0.6",
            "172.18.0.7",
        ],
        db_name="fastapi_core",  # Keyspace name
        db_port=9042,  # optional (default 9042)
        protocol_version=4,  # optional(default 4)
        consistency_level=ConsistencyLevel.QUORUM,  # optional (default QUORUM)
    )
    # connecting to the scylla db
    scylla_manager.connect_to_scylla_db()
    # connecting to the redis
    cache_manager = RedisCacheManager(
        host="redis",
        port=6379,
        passwd=None,  # optional (default None)
        db=0,  # optional (default 0)
        charset="utf-8",  # optional (default utf-8)
        decode_resp=True  # optional (default True)
    )
    cache_manager.connect_to_cache_db()

    yield
    # Shutting down every connection we took
    scylla_manager.shutdown_scylla_db()
    cache_manager.close_cache_db_connection()
```

### Defining your first model/table

* You need to extend the `fastapi_core.scylla.AsyncModel` or `fastapi_core.scylla.BaseModel`. Here is an example,

```python
class BkashPackage(AsyncModel):
    __keyspace__ = KEYSPACE
    __table_name__ = "bkash_packages"

    language = columns.Text(max_length=16, partition_key=True, required=True)
    name = columns.Text(max_length=32, primary_key=True, required=True)
    latest_version = columns.Float(required=False)
```

* Now it's time to reflect in db. To do that, there is a utility function named `ScyllaUtils.sync_tables`
  from `fastapi_core.scylla.utils`. Let's see how we can use that.

```python
def sync_table_to_scylla_db():
    ScyllaUtils.sync_tables(
        keyspace=KEYSPACE,  # Keyspace name
        # All the models extended from BaseModel OR AsyncModel
        models=(BkashPackage,),
        # How many seconds will wait to sync next model. This is needed for gossip based syncing procedure.
        # If you are confused then keep it some value. it'll slow to sync but safe.
        # If your Scylla >= 5.2 then you can set a minimum value or 0.
        wait_for_next=1,
    )
```

* Now just call the function from script/startup or anywhere if you think it's suitable.

### Query from Scylla DB

* We are using repository pattern for accessing the databases. See the example how things going...

```python
async def scylla_crud_operations():
    package_repo = BaseScyllaAsyncRepo(model_class=BkashPackage)
    logger.debug("Create some dummy data to scylla db", extra=log_data)
    _dummy_data = {
        "language": "Python",
        "name": "bk-fastapi-aura",
        "latest_version": "1.0.0",
    }
    await package_repo.create(data=_dummy_data)

    logger.debug("Retrieving the data from scylla", extra=log_data)
    data_from_db = await package_repo.list(query={})
    # Here is the data from db
    print(data_from_db)

    logger.debug("Updating the data on scylla", extra=log_data)
    _existing_data = await package_repo.get(
        query={"language": "Python", "name": "bk-fastapi-aura"}
    )
    _existing_data.update(
        **{
            "latest_version": "1.1.5",
            "name": "bk-fastapi-aura",
        },
    )

    logger.debug("Deleting a single item  from scylla", extra=log_data)
    _existing_data.delete()
```

### Query from the Firestore DB.

* You need to create an object from `BaseFirestoreAsyncRepo` OR you can write your own Repo extend from
  this (`BaseFirestoreAsyncRepo`)

```python
async def firestore_crud_operations():
    log_data: dict = {"db": "Firestore"}
    # Get the firestore db client
    firestore_client: FirestoreClient = await get_firestore_db(
        project_id=os.environ["FIRESTORE_PROJECT_ID"]
    )
    repo = BaseFirestoreAsyncRepo(
        model_class=FirestoreBkashPackage, db=firestore_client
    )
    logger.debug("Creating dummy data to firestore.", extra=log_data)
    for d in DUMMY_DATA:
        await repo.create(document=d["name"], data=d)
    created_data = await repo.list(query={})
    # Here is the list of newly created data.
    print(created_data)

    logger.debug("Going to update some data.", extra=log_data)
    await repo.update(
        document=DUMMY_DATA[0]["name"],
        data={**DUMMY_DATA[0], "latest_version": "2.0.5"},
    )

    logger.debug("Going to delete an entry.", extra=log_data)
    await repo.delete(document=DUMMY_DATA[0]["name"])
```

### Cache your data to redis cache.

We already connected the redis to our application. So, it's time use the cache.
So, you can just use the `CacheRepo` or extend the class on you own purpose.

```python
def test_redis_cache():
    cache_client = RedisCacheManager.get_cache_client()
    log_data: dict = {"db": "Redis Cache"}
    cache_repo = CacheRepo(cache_client)
    logger.debug("Setting item to cache", extra=log_data)
    _key: str = "test-redis"
    _cache_data: str = "I am Redis!"
    cache_repo.set(key=_key, value=_cache_data)

    logger.debug("Getting item to cache", extra=log_data)
    data = cache_repo.get(key=_key)
    # Here is the cache data
    print(data)
```

### Setting up the default logging.

Logging setup is really easy. Here is an example.

```python
logger: Logger = CoreLogger.get_logger(
    # All the parameters are optional.
    log_format="%(asctime)s | %(levelname)s: %(message)s | custom_data: %(custom)s",
    handler=StreamHandler(sys.stdout),
    log_level=logging.DEBUG,
    name="Readme",
    propagate=False,
)
# This is how we can log now :)
logger.info("hello logging.", extra={"custom_data": "I am custom data."})
```

### Register exception handler

This how we can register default exception handler to the project.

```python
from fastapi.exceptions import RequestValidationError
from bk_fastapi_core.exceptions import (
    APIException,
    AppException,
    app_exception_handler,
    http_exception_handler,
    request_validation_exception_handler,
)


@app.exception_handler(APIException)
async def custom_http_exception_handler(request, e):
    return await http_exception_handler(request, e)


@app.exception_handler(RequestValidationError)
async def custom_validation_exception_handler(request, e):
    return await request_validation_exception_handler(request, e)


@app.exception_handler(AppException)
async def custom_app_exception_handler(request, e):
    return await app_exception_handler(request, e)
```

### Register Middlewares

We have few middlewares available for now. You can add like following. If you need them,

```python
from bk_fastapi_core.middlewares import LoggingMiddleware, RequestIDMiddleware

app.add_middleware(RequestIDMiddleware)
app.add_middleware(
    LoggingMiddleware,
    logger=logger,
    log_data=DEFAULT_LOG_DATA,
    # Also you can control your log level
    log_level=logging.DEBUG
)
```

## Contributing Guide

### Run Locally
```shell
docker compose up
```

### Before you commit
```shell
$ poetry run pre-commit install
$ poetry run pre-commit install --hook-type commit-msg
```
### How to publish the package?

We are going to publish the package to [artifacthub](https://artifacthub.bkash.com/). Follow the steps to do so.

* Run `poetry build`
* Add the repository to poetry `pyproject.toml`

```shell
poetry source add --priority=supplemental artifacthub https://artifacthub.bkash.com/repository/python-hosted/
```

**OR**

```toml
[[tool.poetry.source]]
name = "artifacthub"
url = "https://artifacthub.bkash.com/repository/python-hosted/"
priority = "supplemental"
```

* Add the credentials config for the repository.

```shell
# poetry config http-basic.<name of you repo> <your username> <password>
poetry config http-basic.artifacthub username password
```

* Upload the package

```shell
poetry publish --repository artifacthub
```

**Your latest version will be uploaded to the repository.**