Skip to content

A CRUD api application for user events tracking built with python on the Blacksheep framework with a postgresql database

Notifications You must be signed in to change notification settings

hannahmneal/user_event_tracking_app

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

9 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

User Event Tracking Api Application

🌐 API

βš™οΈ Setup

Pre-requisites (all users)

  • Download and install Docker Desktop via Docker Desktop for Mac or Docker Desktop for Windows. (❗This may require a machine restart if you have not downloaded it before.)
  • Clone this repo and open the project in your IDE (I am using Visual Studio Code)
Mac Users

This was tested on a Macbook Pro (M2) running Ventura 13.4.1

  • To get started, from the project root create a .env file and copy-🍝 the contents of .env.sample into it (refer to the "Environment Variables" table below for local values)
  • Create your virtual environment (for venv users, this is: python3 -m venv api && source api/bin/activate)
  • Still from the root, run make api-start-local to set up the database, load requirements, and start the api.
  • At the time of this writing, the database will be created without any stub data to use. It's important to note that, due to the linkage between Person and Event, you will need to create some Person rows before creating an Event. This can be done easily if you have Postman installed and upload the collection I included in api/docs/User Events Api Application.postman_collection.json.

Windows Users

This was tested on a Samsung machine (64-bit) running Windows 11

  • To get started, from the project root create a .env file and copy-🍝 the contents of .env.sample into it (refer to the "Environment Variables" table below for local values)
  • Create your virtual environment (for venv users, this is: python3 -m venv api, then api/Scripts/activate)
  • Still from the root, run bash startup.sh to set up the database, load requirements, and start the api (see "Troubleshooting Setup" in api/docs/troubleshooting_setup.md below if you receive errors)
  • At the time of this writing, the database will be created without any stub data to use. It's important to note that, due to the linkage between Person and Event, you will need to create some Person rows before creating an Event. This can be done easily if you have Postman installed and upload the collection I included in api/docs/User Events Api Application.postman_collection.json.

πŸ‘‡ Click the dropdown tables below for additional resources

Environment Variables

While there is only one environment currently (local), variables for database connections, authentication, and other sensitive information would need to be changed for non-local development.

Env Variable Local Value Description & Usage
DB_HOST "localhost" The host for the PostgreSQL database
DB_NAME "postgres" The name of the PostgreSQL database
DB_PASS "password" The password for the PostgreSQL database
DB_PORT 6543 The port for the PostgreSQL database
DB_USER "dev" The user of the PostgreSQL database (PostgreSQL requires this)
ENVIRONMENT "local" Denotes the current development environment
SHOW_ERROR_DETAILS false Allows exception details to be surfaced directly from failing web requests as described here. To avoid security issues, this is false by default and will only be true if ENVIRONMENT="local"

Tasks & Commands
Command Description
make api-start-local Sets up the database in a Docker container, loads requirements, and starts the api locally. This is a composite command consisting of setup-db, load-requirements, and start-api.
make load-requirements Loads requirements from requirements.txt.
make setup-db Sets up a PostgreSQL database in a Docker container.
make start-api Starts the api (assuming the database is already running successfully and requirements are loaded).
bash startup.sh A shell command equivalent of make api-start-local; Intended for Windows users (or anyone not using a Makefile) to reduce annoying installations and setup but it can be used by Mac users as well.

πŸ”— Routes & Requests

OpenApi Documentation can be found locally at http://127.0.0.1:8080/docs. Additionally, a Postman Collection has been included for convenience (see api/docs/User Events Api Application.postman_collection.json). Import it directly into Postman to begin playing with the api. It includes collection variables and pre-request scripts for dynamic POST requests, but it does not include environment variables. You will need to add an environment variable yourself called base_url. It's value should be http://127.0.0.1:8080.

For those who don't have Postman, Routes & Requests are included in the dropdowns below

GET Persons

Route: http://127.0.0.1:8080/persons

Response:

{
    "data": [
        {
            "id": "f4cc4f34-3f7c-4a0c-b333-df23f72ebc68",
            "datetime_created": "2023-10-08T06:03:33.330037",
            "datetime_modified": "2023-10-08T06:03:33.330037",
            "email": "Max.Rolfson@test.mock",
            "events": [
                {
                    "id": "3b027982-2fe9-4491-a5a2-b504b0e2bb8d",
                    "person_id": "f4cc4f34-3f7c-4a0c-b333-df23f72ebc68",
                    "event_type": "signup",
                    "datetime_created": "2023-10-08 06:03:33.334854"
                }
            ],
            "first_name": "Max",
            "last_name": "Rolfson",
            "role": "user"
        },
        {
            "id": "40a349f1-35d7-48d7-aa09-bb0afdd35e3e",
            "datetime_created": "2023-10-07T23:36:07.478145",
            "datetime_modified": "2023-10-07T19:24:37.085629",
            "email": "Jean.Grey@phoenix.mock",
            "events": [
                {
                    "id": "5d114a9c-9dab-4ceb-8c8e-25fe30b4f099",
                    "person_id": "40a349f1-35d7-48d7-aa09-bb0afdd35e3e",
                    "event_type": "click",
                    "datetime_created": "2023-10-07T23:36:54.430671"
                },
                {
                    "id": "d8b769c7-aa0b-4ddc-b63e-42fdbaa3981e",
                    "person_id": "40a349f1-35d7-48d7-aa09-bb0afdd35e3e",
                    "event_type": "click",
                    "datetime_created": "2023-10-07T23:36:55.224951"
                },
                {
                    "id": "d156191e-f073-45f6-9031-c4b323fa1666",
                    "person_id": "40a349f1-35d7-48d7-aa09-bb0afdd35e3e",
                    "event_type": "signup",
                    "datetime_created": "2023-10-07T23:36:56.114097"
                },
                {
                    "id": "3669c226-b6ca-4275-a68a-d447ca15cf88",
                    "person_id": "40a349f1-35d7-48d7-aa09-bb0afdd35e3e",
                    "event_type": "signup",
                    "datetime_created": "2023-10-07T23:36:56.935621"
                }
            ],
            "first_name": "Jean",
            "last_name": "Grey",
            "role": "admin"
        },
        {
            "id": "9f5e7b72-3268-44c0-b5a7-98fed5b8d740",
            "datetime_created": "2023-10-08T05:39:36.671925",
            "datetime_modified": "2023-10-08T00:57:48.993778",
            "email": "scott.summers@themoon.mock",
            "events": [
                {
                    "id": "0323d885-13a9-460e-96ab-001afc534fcb",
                    "person_id": "9f5e7b72-3268-44c0-b5a7-98fed5b8d740",
                    "event_type": "signup",
                    "datetime_created": "2023-10-08T05:39:36.683119"
                },
                {
                    "id": "7b20a3bd-002b-4d58-941f-c79011ba3e1a",
                    "person_id": "9f5e7b72-3268-44c0-b5a7-98fed5b8d740",
                    "event_type": "click",
                    "datetime_created": "2023-10-08T00:36:54.430671"
                },
                {
                    "id": "d7584bb53-bef2-49c2-8490-8780c85d8744",
                    "person_id": "9f5e7b72-3268-44c0-b5a7-98fed5b8d740",
                    "event_type": "click",
                    "datetime_created": "2023-10-08T00:58:55.224951"
                }
            ],
            "first_name": "Scott",
            "last_name": "Summers",
            "role": "admin"
        }
    ],
    "response": {
        "details": "The request was successful",
        "message": "Ok",
        "status": 200
    }
}

GET Person by id

Route: http://127.0.0.1:8080/persons/{id}

Response:

{
    "data": {
        "id": "9f5e7b72-3268-44c0-b5a7-98fed5b8d740",
        "datetime_created": "2023-10-08T05:39:36.671925",
        "datetime_modified": "2023-10-08T00:57:48.993778",
        "email": "scott.summers@themoon.mock",
        "events": [
            {
                "id": "0323d885-13a9-460e-96ab-001afc534fcb",
                "person_id": "9f5e7b72-3268-44c0-b5a7-98fed5b8d740",
                "event_type": "signup",
                "datetime_created": "2023-10-08T05:39:36.683119"
            },
            {
                "id": "7b20a3bd-002b-4d58-941f-c79011ba3e1a",
                "person_id": "9f5e7b72-3268-44c0-b5a7-98fed5b8d740",
                "event_type": "click",
                "datetime_created": "2023-10-08T00:36:54.430671"
            },
            {
                "id": "d7584bb53-bef2-49c2-8490-8780c85d8744",
                "person_id": "9f5e7b72-3268-44c0-b5a7-98fed5b8d740",
                "event_type": "click",
                "datetime_created": "2023-10-08T00:58:55.224951"
            }
        ],
        "first_name": "Scott",
        "last_name": "Summers",
        "role": "admin"
    },
    "response": {
        "details": "The request was successful",
        "message": "Ok",
        "status": 200
    }
}

POST Persons

Route: http://127.0.0.1:8080/persons

Request body:

{
  "email": "kate.pryde@marauders.mock",
  "first_name": "Kate",
  "last_name": "Pryde",
  "role": 'admin'
}

Response:

{
    "data": {
        "id": "f4cc4f34-3f7c-4a0c-b333-df23f72ebc68",
        "datetime_created": "2023-10-08T06:03:33.330037",
        "datetime_modified": "2023-10-08T06:03:33.330037",
        "email": "kate.pryde@marauders.mock",
        "events": [
            {
                "id": "3b027982-2fe9-4491-a5a2-b504b0e2bb8d",
                "datetime_created": "2023-10-08T06:03:33.334854",
                "event_type": "signup",
                "person_id": "f4cc4f34-3f7c-4a0c-b333-df23f72ebc68"
            }
        ],
        "first_name": "Kate",
        "last_name": "Pryde",
        "role": "admin"
    },
    "response": {
        "details": "The request was successful",
        "message": "Ok",
        "status": 201
    }
}

DELETE Persons

Route: http://127.0.0.1:8080/persons/{id}

Request body:

{
    "id": "7c56a5bc-6037-432f-bd4e-3606a744fcf4"
}

Response:

{
    "data": null,
    "response": {
        "details": "The request was successful",
        "message": "Ok",
        "status": 200
    }
}


GET Events

Route: http://127.0.0.1:8080/events

Params (optional):

  • ?keyword={event_type} : Use a keyword to search events
  • ?person_id={person_id}: Use a person_id (person.id) to search events
  • Example: http://127.0.0.1:8080/events?keyword=click&person_id=40a349f1-35d7-48d7-aa09-bb0afdd35e3e

Response (no params):

{
    "data": [
        {
            "id": "bf8f9aa1-94b9-4261-9ccb-a35e279d2af2",
            "datetime_created": "2023-10-08T06:21:29.214489",
            "event_type": "submitted_feedback",
            "person_id": "9f5e7b72-3268-44c0-b5a7-98fed5b8d740"
        },
        {
            "id": "cb9ce32a-6571-4e7c-8f6d-ffbdd31aade2",
            "datetime_created": "2023-10-08T20:00:37.769065",
            "event_type": "signup",
            "person_id": "427d6a9e-7d7c-4dc7-a2e5-f38128421ab9"
        },
        {
            "id": "f5006d03-aad9-4b23-aa14-9152de9e05dd",
            "datetime_created": "2023-10-08T20:05:49.174802",
            "event_type": "signup",
            "person_id": "ea880a90-d96f-4d80-8bbe-71436f12cd4f"
        },
        {
            "id": "5d114a9c-9dab-4ceb-8c8e-25fe30b4f099",
            "datetime_created": "2023-10-07T23:36:54.430671",
            "event_type": "click",
            "person_id": "40a349f1-35d7-48d7-aa09-bb0afdd35e3e"
        },
        {
            "id": "d8b769c7-aa0b-4ddc-b63e-42fdbaa3981e",
            "datetime_created": "2023-10-07T23:36:55.224951",
            "event_type": "click",
            "person_id": "40a349f1-35d7-48d7-aa09-bb0afdd35e3e"
        },
        {
            "id": "d156191e-f073-45f6-9031-c4b323fa1666",
            "datetime_created": "2023-10-07T23:36:56.114097",
            "event_type": "signup",
            "person_id": "40a349f1-35d7-48d7-aa09-bb0afdd35e3e"
        },
        {
            "id": "3669c226-b6ca-4275-a68a-d447ca15cf88",
            "datetime_created": "2023-10-07T23:36:56.935621",
            "event_type": "signup",
            "person_id": "40a349f1-35d7-48d7-aa09-bb0afdd35e3e"
        },
        {
            "id": "0323d885-13a9-460e-96ab-001afc534fcb",
            "datetime_created": "2023-10-08T05:39:36.683119",
            "event_type": "signup",
            "person_id": "9f5e7b72-3268-44c0-b5a7-98fed5b8d740"
        }
    ],
    "response": {
        "details": "The request was successful",
        "message": "Ok",
        "status": 200
    }
}

GET Events by id

Route: http://127.0.0.1:8080/events/{id}

Response:

{
    "data": {
        "id": "d8b769c7-aa0b-4ddc-b63e-42fdbaa3981e",
        "datetime_created": "2023-10-07T23:36:55.224951",
        "event_type": "click",
        "person_id": "40a349f1-35d7-48d7-aa09-bb0afdd35e3e"
    },
    "response": {
        "details": "The request was successful",
        "message": "Ok",
        "status": 200
    }
}

POST Events

Route: http://127.0.0.1:8080/events/{id}

Request body:

{
  "event_type": "signup",
  "person_id": "9c65af1a-c109-4c17-9bf1-5f4bcac95e3c"
}

Response:

{
    "data": {
        "id": "9c65af1a-c109-4c17-9bf1-5f4bcac95e3c",
        "datetime_created": "2023-10-06T10:53:31.283195",
        "event_type": "signup",
        "person_id": "0fcf3634-9b0c-4ceb-ab53-7ba8edf3d5fa"
    },
    "response": {
        "details": "The request was successful",
        "message": "Ok",
        "status": 201
    }
}

DELETE Events

Route: http://127.0.0.1:8080/events/{id}

Request body:

{
    "id": "62701308-809d-4302-b345-92ca72285194",
    "person_id": "049bb5dd-91d2-464e-b049-07da3e8d2627"
}

Response:

{
    "data": null,
    "response": {
        "details": "The request was successful",
        "message": "Ok",
        "status": 200
    }
}


πŸ—„οΈ Data & Migrations

Migrations are managed with the piccolo package.

New migrations need to be created whenever there is a change to the database schema. To create a new migration, use the following (always include a brief description): piccolo migrations new db --desc="good description of migration here".

To apply the migration, use piccolo migrations forwards all.


πŸ’¬ DISCUSSION

Originally I wanted to create an api that allowed for the creation of both events and users. I already had a boilerplate CRUD api that I was working on and it had Person objects so I thought, "Why not?". For this reason, Persons are included in the project. The linkage between Person and Event isn't complete but I'm still working on that. I focused my attention on the Event functionality, which includes the ability to search by a keyword.

I chose blacksheep for my framework because (A) I was already playing with it, (B) it's new and different to me, and (C) I am fascinated by its blend of Python + .NET. It seems like the best of both worlds and I was intrigued by the promise of maintainability, type safety, and speed of performance. I used Piccolo ORM for database interactions because it was recommended by Blacksheep's Roberto Prevato but also because after a galumping through SQLAlchemy docs for what felt like decades, 30 minutes spent on Piccolo's offerings convinced me it was feature-rich and worth trying (the auto_increment option for a Timestamp, the Playground, piccolo admin, and simple Postgres setup are all great examples). For data validation, I am using Pydantic, which handily ships with Piccolo.

As for architecture, I was going for a microservices approach. This is a small project so I began with a single file (server.py) which contains the database connection logic, routing, and 'service layer methods'. I do not enjoy how crowded it has become and I intend to simplify everything by separating routes from service logic and streamlining errors, exceptions, responses and if/else conditions. After doing some light reading about events and event sourcing, I decided to use a very simple Event table structure and a JSONB string which can be searched with the arrow function provided by Piccolo (in the same way as a ->> is used in raw SQL).


πŸ“” RESOURCES & REFERENCES

[1] Simple CRUD REST API with Blacksheep and Piccolo ORM by Carlos Armando Marcano Vargas

[2] Auto Migrations (from the Piccolo blog)

[3] Piccolo ORM

[4] "How to Decide What Events to Track (+15 Examples)" by Levi Olmstead on Whatfix Blog

[5] "Event Storage in Postgres" by Kasey Speakman

[6] "Building an Event Storage" by Kasey Speakman


About

A CRUD api application for user events tracking built with python on the Blacksheep framework with a postgresql database

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages