commit 87c81fd7c032212f43a5b93f7ae276200c5db9e4 Author: Tim Kächele Date: Mon Apr 21 09:28:33 2025 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a072359 --- /dev/null +++ b/.gitignore @@ -0,0 +1,143 @@ +# IDE +.idea + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Custom files +*ready + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d56bed2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: + - repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + name: isort (python) + args: + - --profile=black + - repo: https://github.com/psf/black + rev: 22.6.0 + hooks: + - id: black + language_version: python3 + types: [ python ] + - repo: https://github.com/myint/autoflake + rev: v1.5.1 + hooks: + - id: autoflake + language_version: python3 + types: [ python ] + args: + - --remove-all-unused-imports + - --recursive + - --in-place \ No newline at end of file diff --git a/app/api.py b/app/api.py new file mode 100644 index 0000000..0083505 --- /dev/null +++ b/app/api.py @@ -0,0 +1,43 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel, Field, condecimal, conint, constr, root_validator + +from app.types import Order, OrderSide, OrderType + +app = FastAPI() + + +class CreateOrderModel(BaseModel): + type_: OrderType = Field(..., alias="type") + side: OrderSide + instrument: constr(min_length=12, max_length=12) + limit_price: Optional[condecimal(decimal_places=2)] + quantity: conint(gt=0) + + @root_validator + def validator(cls, values: dict): + if values.get("type_") == "market" and values.get("limit_price"): + raise ValueError( + "Providing a `limit_price` is prohibited for type `market`" + ) + + if values.get("type_") == "limit" and not values.get("limit_price"): + raise ValueError("Attribute `limit_price` is required for type `limit`") + + return values + + +class CreateOrderResponseModel(Order): + pass + + +@app.post( + "/orders", + status_code=201, + response_model=CreateOrderResponseModel, + response_model_by_alias=True, +) +async def create_order(model: CreateOrderModel): + # TODO: Add your implementation here + raise NotImplementedError() diff --git a/app/stock_exchange.py b/app/stock_exchange.py new file mode 100644 index 0000000..82e043e --- /dev/null +++ b/app/stock_exchange.py @@ -0,0 +1,24 @@ +import random +import time + +from app.types import Order + + +class OrderPlacementError(Exception): + pass + + +def place_order(order: Order): + """dummy function that is symbolic standing for placing an order at the stock exchange. + Please do not modify the content of this method.""" + + if not order: + raise ValueError("Required order parameter not provided") + + if random.random() >= 0.9: + raise OrderPlacementError( + "Failed to place order at stock exchange. Connection not available" + ) + + # it is an expensive operation + time.sleep(0.5) diff --git a/app/types.py b/app/types.py new file mode 100644 index 0000000..26e867c --- /dev/null +++ b/app/types.py @@ -0,0 +1,27 @@ +from datetime import datetime +from enum import Enum +from typing import Optional + +from pydantic import BaseModel, Field, condecimal, conint, constr + + +class OrderSide(Enum): + BUY = "buy" + SELL = "sell" + + +class OrderType(Enum): + MARKET = "market" + LIMIT = "limit" + + +class Order(BaseModel): + # id generated by the database + id_: str = Field(..., alias="id") + created_at: datetime + + type_: OrderType = Field(..., alias="type") + side: OrderSide + instrument: constr(min_length=12, max_length=12) + limit_price: Optional[condecimal(decimal_places=2)] + quantity: conint(gt=0) diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..90d44ca --- /dev/null +++ b/readme.md @@ -0,0 +1,50 @@ +# Backend engineering task + +### Intro + +The repository contains a bare setup for a REST API to place orders via a `POST - /orders` request, it can be found in [./app/api.py](./app/api.py). +Additionally, it contains [a building block](./app/stock_exchange.py) that symbolizes placing an order at the stock exchange. + +The folder [tests](./tests) contains the setup for tests with the framework pytest. + +The challenge in this repository is a simplified version of what we are dealing with at lemon.markets 🍋. + +## Your task + +We would like you to finalize the missing parts in this repository so that the following requirements are fulfilled: +1. A valid request to `POST /orders` should result in the order being stored in a database of your choice. +2. The created order should be placed at the stock exchange (use the method `place_order` provided in stock_exchange.py). +3. The endpoint should return a status code of 201 and the created order details, provided that the order has been saved in the database **AND** it is guaranteed that the order **will** be placed on the stock exchange. +4. In the case of an error in the endpoint, it should return the status code 500 and the body `{"message": "Internal server error while placing the order"}` +5. The API should be highly scalable and reliable. The reliability of the provided stock exchange should not impact the reliability of the `POST /orders` endpoint + + +Additionally, please add some tests and document how you would test the application as a whole and its pieces. +For the implementation you can choose to use this Python setup or implement it in Javascript/Typescript. + +Please include a `solution.md` file where you document your decisions, assumptions, and also improvements you would like to incorporate in the future. + +We value your time ⏰, so we do not expect you to spend more than 4 hours preparing the solution. 🤗 +Focus on implementing the main task first and afterwards jump on additional improvements as you see fit. + +### Bonus tasks +* How would you change the system if we would receive a high volume of async updates to the orders placed through a socket connection on the stock exchange, e.g. execution information? Please outline the changes in the `solution.md`. +* Feel free to add a GitHub actions workflow to test the application. +* Feel free to add a Dockerfile. + +## Code quality + +In order to maintain a certain amount of code quality, we are using pre-commit hooks +in this repository, which are installed by a 3rd-party tool called [pre-commit](https://pre-commit.com/). + +Please follow [this documentation](https://pre-commit.com/#install) to install `pre-commit` +on your local machine. After that just execute the following command to install the hooks +to your git folder: + +```shell +pre-commit install +``` + + +### Everything process related +Please create a fork or clone of this repository, commit all of your work to a new branch, and provide us with a link to the solution via mail 📩, at least 6h before the review meeting. diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..d674265 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +-r requirements.txt +pytest +pytest-cov +pre-commit +requests \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..978d93d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +fastapi==0.85.0 +uvicorn[standard]==0.18.3 +pydantic==1.10.2 \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..2fa9cfb --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +import pytest as pytest +from starlette.testclient import TestClient + +from app.api import app + + +@pytest.fixture +def client() -> TestClient: + return TestClient(app)