├── .gitignore ├── .isort.cfg ├── Dockerfile ├── README.md ├── api ├── .isort.cfg ├── bf_api │ ├── __init__.py │ ├── encoders.py │ ├── endpoints.py │ ├── example_data.py │ ├── injections.py │ ├── main.py │ └── swagger │ │ └── api.yaml ├── mypy.ini ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── setup.py ├── testrequirements.txt └── tox.ini ├── cli └── no_time_to_code_this.py ├── docker-compose.yml ├── k8s ├── README.md ├── helm │ ├── demo-app │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── templates │ │ │ ├── NOTES.txt │ │ │ ├── _helpers.tpl │ │ │ ├── deployment.yaml │ │ │ ├── ingress.yaml │ │ │ ├── registry-credentials.yaml │ │ │ └── service.yaml │ │ └── values.yaml │ └── env │ │ └── staging.yaml ├── locust │ └── locustfile.py └── manifests │ ├── deployment.yaml │ ├── ingress.yaml │ ├── registry-credentials.yaml │ └── svc.yaml ├── ram_db ├── bf_ram_db │ ├── __init__.py │ ├── client.py │ ├── order.py │ ├── product.py │ └── ram_storage.py ├── requirements.txt ├── setup.py └── testrequirements.txt ├── setup.py └── shop ├── .flake8 ├── .isort.cfg ├── bf_shop ├── __init__.py ├── entities.py ├── exceptions.py ├── logic.py └── repositories.py ├── mypy.ini ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── setup.py ├── testrequirements.txt ├── tox.ini └── unittests ├── __init__.py ├── conftest.py └── test_logic.py /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .venv 3 | .idea 4 | .eggs 5 | *.egg-info 6 | *__pycache__ 7 | **/.coverage* -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length=88 3 | known_first_party=bf_shop, bf_ram_db, bf_api 4 | known_standard_library=queue,ast,dataclasses 5 | default_section=THIRDPARTY 6 | skip=.tox 7 | multi_line_output=3 8 | include_trailing_comma=True 9 | force_grid_wrap=0 10 | combine_as_imports=True -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7.0-alpine3.8 2 | 3 | RUN apk --no-cache add bash 4 | 5 | ARG PACKAGE_VERSION=0.0.1+docker 6 | ARG SRC=/opt/app 7 | 8 | ENV SETUPTOOLS_SCM_PRETEND_VERSION ${PACKAGE_VERSION} 9 | ENV PBR_VERSION ${PACKAGE_VERSION} 10 | 11 | ADD . ${SRC} 12 | 13 | RUN pip install ${SRC}/shop 14 | RUN pip install ${SRC}/ram_db 15 | RUN pip install ${SRC}/api 16 | 17 | EXPOSE 8080 18 | 19 | ENTRYPOINT ["shop-api"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clean architecture in Python 3.7 2 | 3 | ## Instalation 4 | For development porpouse you can install all packages in dev mode. In main directory execute: 5 | 6 | ``` 7 | pip install -e . 8 | ``` 9 | 10 | This will install all mico-libs with their requirements. You don't need to install them one by one. 11 | 12 | ## Running 13 | 14 | You can run direclty main function from the `api` package: 15 | 16 | ``` 17 | cd api/bf_api 18 | python main.py 19 | ``` 20 | 21 | or run entry point: 22 | 23 | ``` 24 | shop-api 25 | ``` 26 | -------------------------------------------------------------------------------- /api/.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length=88 3 | known_first_party=bf_api,bf_shop 4 | known_standard_library=queue,ast,dataclasses 5 | default_section=THIRDPARTY 6 | skip=.tox 7 | multi_line_output=3 8 | include_trailing_comma=True 9 | force_grid_wrap=0 10 | combine_as_imports=True -------------------------------------------------------------------------------- /api/bf_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkocjan/python_architecture/a85a1789bf1c81e12511b3cbf8953500fef79e56/api/bf_api/__init__.py -------------------------------------------------------------------------------- /api/bf_api/encoders.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from json import JSONEncoder 3 | from typing import Any 4 | 5 | from bf_shop.entities import BaseEntity, Client 6 | 7 | 8 | class ApiJsonEncoder(JSONEncoder): 9 | def default(self, obj: Any) -> Any: 10 | if isinstance(obj, (datetime.datetime, datetime.date)): 11 | serial = obj.isoformat() 12 | return serial 13 | 14 | if isinstance(obj, Client): 15 | return {"name": obj.name} 16 | 17 | if isinstance(obj, BaseEntity): 18 | return {key: value for key, value in vars(obj).items() if value is not None} 19 | 20 | return JSONEncoder.default(self, obj) 21 | -------------------------------------------------------------------------------- /api/bf_api/endpoints.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from bf_shop.entities import Order 4 | from bf_shop.logic import OrderLogic 5 | 6 | 7 | def search(logic: OrderLogic, client_id: int) -> List[Order]: 8 | return logic.search(client_id=client_id) 9 | 10 | 11 | def create(logic: OrderLogic, body: dict) -> Order: 12 | return logic.create(client_id=body['client_id']) 13 | 14 | 15 | def add_product(logic: OrderLogic, order_id: int, product_id: int) -> Order: 16 | return logic.add_product(order_id, product_id) 17 | -------------------------------------------------------------------------------- /api/bf_api/example_data.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from bf_shop.entities import Client, Order, Product 4 | 5 | clients = [ 6 | Client(id=1, name="Guido van Rossum"), 7 | Client(id=2, name="John Doe") 8 | ] 9 | 10 | products = [ 11 | Product(id=1, name="Phone", price=999), 12 | Product(id=2, name="Graphic Card", price=5499), 13 | ] 14 | 15 | orders = [Order(id=1, created=datetime.now(), client=clients[0], total_cost=0)] 16 | -------------------------------------------------------------------------------- /api/bf_api/injections.py: -------------------------------------------------------------------------------- 1 | import injector 2 | 3 | from bf_shop.repositories import ClientRepository, OrderRepository, ProductRepository 4 | 5 | 6 | class LogicModule(injector.Module): 7 | def configure(self, binder: injector.Binder) -> None: 8 | from bf_shop.logic import OrderLogic 9 | 10 | binder.bind(OrderLogic, to=OrderLogic, scope=injector.SingletonScope) 11 | 12 | 13 | class MemoryProvidersModule(injector.Module): 14 | @injector.singleton 15 | @injector.provider 16 | def provide_clients(self) -> ClientRepository: 17 | from bf_ram_db.client import ClientRamRepository 18 | from bf_api.example_data import clients 19 | 20 | return ClientRamRepository(static_data=clients) 21 | 22 | @injector.singleton 23 | @injector.provider 24 | def provide_orders(self) -> OrderRepository: 25 | from bf_ram_db.order import OrderRamRepository 26 | from bf_api.example_data import orders 27 | 28 | return OrderRamRepository(static_data=orders) 29 | 30 | @injector.singleton 31 | @injector.provider 32 | def provide_products(self) -> ProductRepository: 33 | from bf_ram_db.product import ProductRamRepository 34 | from bf_api.example_data import products 35 | 36 | return ProductRamRepository(static_data=products) 37 | -------------------------------------------------------------------------------- /api/bf_api/main.py: -------------------------------------------------------------------------------- 1 | import connexion 2 | import flask 3 | from connexion.apis.flask_api import FlaskApi 4 | from flask_injector import FlaskInjector 5 | 6 | from bf_api.encoders import ApiJsonEncoder 7 | from bf_api.injections import LogicModule, MemoryProvidersModule 8 | from bf_shop.exceptions import ElementNotFound 9 | 10 | 11 | def main() -> None: 12 | app = connexion.FlaskApp(__name__, specification_dir="swagger/") 13 | app.add_api("api.yaml") 14 | 15 | @app.app.errorhandler(ElementNotFound) 16 | def handle_offer_mgr_exception(exc) -> flask.Response: 17 | response = connexion.problem(status=400, title=exc.message, detail=exc.detail) 18 | 19 | return FlaskApi.get_response(response) 20 | 21 | flask_injector = FlaskInjector( 22 | app=app.app, modules=[LogicModule, MemoryProvidersModule] 23 | ) 24 | app.app.config["FLASK_INJECTOR"] = flask_injector 25 | 26 | app.app.json_encoder = ApiJsonEncoder 27 | app.run(port=8080, debug=True) 28 | 29 | 30 | if __name__ == "__main__": 31 | main() 32 | -------------------------------------------------------------------------------- /api/bf_api/swagger/api.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | 4 | info: 5 | title: "The shop" 6 | description: "" 7 | version: "1.0" 8 | 9 | 10 | basePath: /api/ 11 | 12 | 13 | parameters: 14 | clientIdParam: 15 | name: 'client_id' 16 | in: 'query' 17 | type: integer 18 | required: true 19 | 20 | putOrderBody: 21 | name: 'body' 22 | in: 'body' 23 | required: true 24 | schema: 25 | $ref: '#/definitions/putOrderBody' 26 | 27 | orderIdParam: 28 | name: 'order_id' 29 | in: 'path' 30 | type: integer 31 | required: true 32 | 33 | productIdParam: 34 | name: 'product_id' 35 | in: 'path' 36 | type: integer 37 | required: true 38 | 39 | paths: 40 | /orders/search/: 41 | get: 42 | tags: 43 | - orders 44 | operationId: bf_api.endpoints.search 45 | parameters: 46 | - $ref: '#/parameters/clientIdParam' 47 | responses: 48 | '200': 49 | description: 'Fetch list of client orders' 50 | schema: 51 | type: array 52 | items: 53 | $ref: '#/definitions/Order' 54 | 55 | /orders/create/: 56 | post: 57 | tags: 58 | - orders 59 | operationId: bf_api.endpoints.create 60 | parameters: 61 | - $ref: '#/parameters/putOrderBody' 62 | responses: 63 | '200': 64 | description: 'Create new order' 65 | schema: 66 | $ref: '#/definitions/Order' 67 | 68 | /orders/{order_id}/add_product/{product_id}: 69 | put: 70 | tags: 71 | - orders 72 | operationId: bf_api.endpoints.add_product 73 | parameters: 74 | - $ref: '#/parameters/orderIdParam' 75 | - $ref: '#/parameters/productIdParam' 76 | responses: 77 | '200': 78 | description: 'Add new product to existing order' 79 | schema: 80 | $ref: '#/definitions/Order' 81 | 82 | definitions: 83 | Client: 84 | type: object 85 | properties: 86 | id: { type: 'integer' } 87 | name: { type: 'string' } 88 | 89 | Product: 90 | type: object 91 | properties: 92 | id: { type: 'integer' } 93 | name: { type: 'string' } 94 | price: { type: 'number' } 95 | 96 | Order: 97 | type: object 98 | properties: 99 | id: { type: integer } 100 | created: { type: string } 101 | client: { $ref: '#/definitions/Client' } 102 | total_cost: { type: number } 103 | items: 104 | type: 'array' 105 | items: 106 | $ref: '#/definitions/Product' 107 | 108 | putOrderBody: 109 | type: object 110 | properties: 111 | client_id: 112 | type: integer 113 | example: 1 114 | required: 115 | - client_id -------------------------------------------------------------------------------- /api/mypy.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkocjan/python_architecture/a85a1789bf1c81e12511b3cbf8953500fef79e56/api/mypy.ini -------------------------------------------------------------------------------- /api/pyproject.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkocjan/python_architecture/a85a1789bf1c81e12511b3cbf8953500fef79e56/api/pyproject.toml -------------------------------------------------------------------------------- /api/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs = .* caches 3 | 4 | addopts = 5 | --flake8 6 | --isort 7 | -l 8 | --cache-clear 9 | --cov-report term-missing 10 | --cov=bf_core 11 | 12 | testpaths = bf_core unittests 13 | -------------------------------------------------------------------------------- /api/requirements.txt: -------------------------------------------------------------------------------- 1 | bf_shop 2 | bf_ram_db 3 | flask==1.0.2 4 | connexion==1.5.3 5 | Flask-Injector==0.10.1 -------------------------------------------------------------------------------- /api/setup.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from setuptools import find_packages, setup 4 | 5 | lib_name = "bf_api" 6 | 7 | 8 | def get_requirements(prefix: str = "") -> List[str]: 9 | return [ 10 | x.strip() for x in open("{}requirements.txt".format(prefix), "r").readlines() 11 | ] 12 | 13 | 14 | extra_requires = {"test": get_requirements(prefix="test")} 15 | 16 | setup( 17 | name=lib_name, 18 | packages=find_packages(), 19 | install_requires=get_requirements(), 20 | extra_requires=extra_requires, 21 | package_data={lib_name: ["swagger/*.yaml"]}, 22 | python_requires=">=3.7", 23 | use_scm_version={"root": "..", "relative_to": __file__}, 24 | setup_requires=["pip>=10", "setuptools>=40", "setuptools_scm"], 25 | entry_points={"console_scripts": ["shop-api = bf_api.main:main"]} 26 | ) -------------------------------------------------------------------------------- /api/testrequirements.txt: -------------------------------------------------------------------------------- 1 | pytest==3.9.2 -------------------------------------------------------------------------------- /api/tox.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkocjan/python_architecture/a85a1789bf1c81e12511b3cbf8953500fef79e56/api/tox.ini -------------------------------------------------------------------------------- /cli/no_time_to_code_this.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkocjan/python_architecture/a85a1789bf1c81e12511b3cbf8953500fef79e56/cli/no_time_to_code_this.py -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | webfront: 4 | build: . 5 | volumes: 6 | - .:/opt/app 7 | ports: 8 | - "8080:8080" 9 | -------------------------------------------------------------------------------- /k8s/README.md: -------------------------------------------------------------------------------- 1 | 1. dockerfile 2 | 2. manifests walkthrough 3 | opowiedz czym sa: 4 | - pody 5 | - deployments 6 | - svc 7 | - ingress 8 | wspomnij o: 9 | - hpa 10 | - statefullset 11 | - volumes 12 | - jobs/cronjobs 13 | 14 | dzialania z manifestami 15 | `kubectl apply -f manifests` 16 | `kubectl delete -f manifests` 17 | issues: multiple envs require copies... boring 18 | 19 | 3. helm 20 | instalacja apki 21 | `helm upgrade --install demo-app -f helm/demo-app/values.yaml --debug --dry-run helm/demo-app` 22 | 23 | bump wersji 24 | `helm upgrade --install demo-app -f helm/demo-app/values.yaml -f helm/env/staging.yaml --debug --dry-run helm/demo-app` 25 | 26 | helm list 27 | 28 | historia rewizji 29 | `helm history demo-app` 30 | `helm rollback demo-app 1` 31 | `helm history demo-app` 32 | `helm get manifest demo-app --revision 2` 33 | 34 | 4. Horizontal pod autoscaler: 35 | `kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10` 36 | 37 | 5. locustio 38 | `docker run --rm -v `pwd`:/locust -p 8089:8089 christianbladescb/locustio --host https://demo-app-k8s.testowaplatforma123.net` 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /k8s/helm/demo-app/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | -------------------------------------------------------------------------------- /k8s/helm/demo-app/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: A Helm chart for Kubernetes 4 | name: demo-app 5 | version: 0.1.0 6 | -------------------------------------------------------------------------------- /k8s/helm/demo-app/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range .Values.ingress.hosts }} 4 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} 5 | {{- end }} 6 | {{- else if contains "NodePort" .Values.service.type }} 7 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "demo-app.fullname" . }}) 8 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 9 | echo http://$NODE_IP:$NODE_PORT 10 | {{- else if contains "LoadBalancer" .Values.service.type }} 11 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 12 | You can watch the status of by running 'kubectl get svc -w {{ template "demo-app.fullname" . }}' 13 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "demo-app.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 14 | echo http://$SERVICE_IP:{{ .Values.service.port }} 15 | {{- else if contains "ClusterIP" .Values.service.type }} 16 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "demo-app.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 17 | echo "Visit http://127.0.0.1:8080 to use your application" 18 | kubectl port-forward $POD_NAME 8080:80 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /k8s/helm/demo-app/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "demo-app.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "demo-app.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "demo-app.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | -------------------------------------------------------------------------------- /k8s/helm/demo-app/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1beta2 2 | kind: Deployment 3 | metadata: 4 | name: {{ template "demo-app.fullname" . }} 5 | labels: 6 | app: {{ template "demo-app.name" . }} 7 | chart: {{ template "demo-app.chart" . }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | demo: app 11 | spec: 12 | replicas: {{ .Values.replicaCount }} 13 | selector: 14 | matchLabels: 15 | app: {{ template "demo-app.name" . }} 16 | release: {{ .Release.Name }} 17 | template: 18 | metadata: 19 | labels: 20 | app: {{ template "demo-app.name" . }} 21 | release: {{ .Release.Name }} 22 | spec: 23 | containers: 24 | - name: {{ .Chart.Name }} 25 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 26 | imagePullPolicy: {{ .Values.image.pullPolicy }} 27 | ports: 28 | - name: http 29 | containerPort: 8080 30 | protocol: TCP 31 | resources: 32 | {{ toYaml .Values.resources | indent 12 }} 33 | imagePullSecrets: 34 | - name: registry-credentials 35 | -------------------------------------------------------------------------------- /k8s/helm/demo-app/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "demo-app.fullname" . -}} 3 | {{- $ingressPath := .Values.ingress.path -}} 4 | apiVersion: extensions/v1beta1 5 | kind: Ingress 6 | metadata: 7 | name: {{ $fullName }} 8 | labels: 9 | app: {{ template "demo-app.name" . }} 10 | chart: {{ template "demo-app.chart" . }} 11 | release: {{ .Release.Name }} 12 | heritage: {{ .Release.Service }} 13 | {{- with .Values.ingress.annotations }} 14 | annotations: 15 | {{ toYaml . | indent 4 }} 16 | {{- end }} 17 | spec: 18 | rules: 19 | {{- range .Values.ingress.hosts }} 20 | - host: {{ . }} 21 | http: 22 | paths: 23 | - path: {{ $ingressPath }} 24 | backend: 25 | serviceName: {{ $fullName }} 26 | servicePort: http 27 | {{- end }} 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /k8s/helm/demo-app/templates/registry-credentials.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | .dockerconfigjson: eyJhdXRocyI6IHsicmVnaXN0cnkuZ2l0bGFiLmNvbSI6IHsiYXV0aCI6ICJlbUpwWjI1cFpYY3VabkpoZEdONllXdEFZbVZsWm1WbExtTnZMblZyT25CTlRFSnBWM2wzU21rMGMzQjZielZ2TlcxTCJ9fX0= 4 | kind: Secret 5 | metadata: 6 | name: registry-credentials 7 | namespace: demo 8 | type: kubernetes.io/dockerconfigjson 9 | -------------------------------------------------------------------------------- /k8s/helm/demo-app/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "demo-app.fullname" . }} 5 | labels: 6 | app: {{ template "demo-app.name" . }} 7 | chart: {{ template "demo-app.chart" . }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | spec: 11 | type: {{ .Values.service.type }} 12 | ports: 13 | - port: {{ .Values.service.port }} 14 | targetPort: 8080 15 | protocol: TCP 16 | name: http 17 | selector: 18 | app: {{ template "demo-app.name" . }} 19 | release: {{ .Release.Name }} 20 | -------------------------------------------------------------------------------- /k8s/helm/demo-app/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for demo-app. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: registry.gitlab.com/bee-fee/betting/sportsbook/demo/app 9 | tag: latest 10 | pullPolicy: Always 11 | 12 | service: 13 | type: ClusterIP 14 | port: 8080 15 | 16 | ingress: 17 | enabled: true 18 | annotations: 19 | kubernetes.io/ingress.class: nginx 20 | nginx.org/ssl-services: demo-app 21 | path: / 22 | hosts: 23 | - demo-app-k8s.testowaplatforma123.net 24 | 25 | resources: 26 | limits: 27 | cpu: 200m 28 | memory: 256Mi 29 | requests: 30 | cpu: 100m 31 | memory: 128Mi 32 | -------------------------------------------------------------------------------- /k8s/helm/env/staging.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 4 2 | -------------------------------------------------------------------------------- /k8s/locust/locustfile.py: -------------------------------------------------------------------------------- 1 | from locust import HttpLocust, TaskSet, task 2 | 3 | 4 | class StalkerTaskSet(TaskSet): 5 | @task(1) 6 | def get_angelinas_orders(self): 7 | self.client.get("/api/orders/search/", params={'client_id': 1}) 8 | 9 | class DemoLocust(HttpLocust): 10 | task_set = StalkerTaskSet 11 | min_wait = 100 12 | max_wait = 1500 13 | -------------------------------------------------------------------------------- /k8s/manifests/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: demo-app 6 | name: demo-app 7 | namespace: demo 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: demo-app 13 | template: 14 | metadata: 15 | labels: 16 | app: demo-app 17 | spec: 18 | containers: 19 | - image: registry.gitlab.com/bee-fee/betting/sportsbook/demo/app:latest 20 | imagePullPolicy: Always 21 | name: demo-app 22 | ports: 23 | - containerPort: 8080 24 | name: http 25 | protocol: TCP 26 | resources: 27 | limits: 28 | cpu: 200m 29 | memory: 256Mi 30 | requests: 31 | cpu: 100m 32 | memory: 128Mi 33 | imagePullSecrets: 34 | - name: registry-credentials 35 | -------------------------------------------------------------------------------- /k8s/manifests/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | annotations: 5 | kubernetes.io/ingress.class: nginx 6 | nginx.org/ssl-services: demo-app 7 | labels: 8 | app: demo-app 9 | name: demo-app 10 | namespace: demo 11 | spec: 12 | rules: 13 | - host: demo-app-k8s.testowaplatforma123.net 14 | http: 15 | paths: 16 | - backend: 17 | serviceName: demo-app 18 | servicePort: 8080 19 | path: / 20 | -------------------------------------------------------------------------------- /k8s/manifests/registry-credentials.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | .dockerconfigjson: eyJhdXRocyI6IHsicmVnaXN0cnkuZ2l0bGFiLmNvbSI6IHsiYXV0aCI6ICJlbUpwWjI1cFpYY3VabkpoZEdONllXdEFZbVZsWm1WbExtTnZMblZyT25CTlRFSnBWM2wzU21rMGMzQjZielZ2TlcxTCJ9fX0= 4 | kind: Secret 5 | metadata: 6 | name: registry-credentials 7 | namespace: demo 8 | type: kubernetes.io/dockerconfigjson 9 | -------------------------------------------------------------------------------- /k8s/manifests/svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: demo-app 6 | name: demo-app 7 | namespace: demo 8 | spec: 9 | ports: 10 | - name: http 11 | port: 8080 12 | protocol: TCP 13 | targetPort: 8080 14 | selector: 15 | app: demo-app 16 | sessionAffinity: None 17 | type: ClusterIP 18 | -------------------------------------------------------------------------------- /ram_db/bf_ram_db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkocjan/python_architecture/a85a1789bf1c81e12511b3cbf8953500fef79e56/ram_db/bf_ram_db/__init__.py -------------------------------------------------------------------------------- /ram_db/bf_ram_db/client.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from bf_ram_db.ram_storage import RamStorage 4 | from bf_shop.entities import Client 5 | from bf_shop.exceptions import ClientNotFound 6 | from bf_shop.repositories import ClientRepository 7 | 8 | 9 | class ClientRamRepository(ClientRepository): 10 | def __init__(self, static_data: Optional[List[Client]] = None) -> None: 11 | self._ram_storage = RamStorage[Client]() 12 | 13 | if static_data is None: 14 | static_data = [] 15 | 16 | for client in static_data: 17 | self._ram_storage.add(client) 18 | 19 | def create(self, name: str) -> Client: 20 | client_id = self._ram_storage.next_pk() 21 | 22 | self._ram_storage.add(Client(id=client_id, name=name)) 23 | 24 | return self._ram_storage.get(client_id) 25 | 26 | def get(self, client_id: int) -> Client: 27 | result = self._ram_storage.get(client_id) 28 | if result is None: 29 | raise ClientNotFound() 30 | 31 | return result 32 | -------------------------------------------------------------------------------- /ram_db/bf_ram_db/order.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List, Optional 3 | 4 | from bf_ram_db.ram_storage import RamStorage 5 | from bf_shop.entities import Client, Order 6 | from bf_shop.exceptions import OrderNotFound 7 | from bf_shop.repositories import OrderRepository 8 | 9 | 10 | class OrderRamRepository(OrderRepository): 11 | def __init__(self, static_data: Optional[List[Order]] = None) -> None: 12 | self._ram_storage = RamStorage[Order]() 13 | 14 | if static_data is None: 15 | static_data = [] 16 | 17 | for order in static_data: 18 | self._ram_storage.add(order) 19 | 20 | def create(self, client: Client) -> Order: 21 | order_id = self._ram_storage.next_pk() 22 | 23 | self._ram_storage.add(Order(id=order_id, client=client, created=datetime.now())) 24 | 25 | return self._ram_storage.get(order_id) 26 | 27 | def get(self, order_id: int) -> Order: 28 | result = self._ram_storage.get(order_id) 29 | if result is None: 30 | raise OrderNotFound() 31 | return result 32 | 33 | def save(self, order: Order) -> Order: 34 | self._ram_storage.add(order) 35 | return order 36 | 37 | def search( 38 | self, client: Optional[Client] = None, created: Optional[datetime] = None 39 | ) -> List[Order]: 40 | storage = self._ram_storage 41 | 42 | if client: 43 | storage = storage.search(client=client) 44 | 45 | if created: 46 | storage = storage.search(created=created) 47 | 48 | return storage.all() 49 | -------------------------------------------------------------------------------- /ram_db/bf_ram_db/product.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from bf_ram_db.ram_storage import RamStorage 4 | from bf_shop.entities import Product 5 | from bf_shop.exceptions import ProductNotFound 6 | from bf_shop.repositories import ProductRepository 7 | 8 | 9 | class ProductRamRepository(ProductRepository): 10 | def __init__(self, static_data: Optional[List[Product]] = None) -> None: 11 | self._ram_storage = RamStorage[Product]() 12 | 13 | if static_data is None: 14 | static_data = [] 15 | 16 | for product in static_data: 17 | self._ram_storage.add(product) 18 | 19 | def get(self, product_id: int) -> Product: 20 | result = self._ram_storage.get(product_id) 21 | if result is None: 22 | raise ProductNotFound() 23 | return result 24 | -------------------------------------------------------------------------------- /ram_db/bf_ram_db/ram_storage.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from functools import reduce 4 | from typing import Any, Dict, Generic, List, Optional, TypeVar 5 | 6 | T = TypeVar("T") 7 | StorageType = Dict[int, T] 8 | 9 | 10 | class RamStorage(Generic[T]): 11 | def __init__(self, storage: StorageType = None, pk_name: str = "id") -> None: 12 | self._pk_name: str = pk_name 13 | self._max_pk: int = 0 14 | if storage is None: 15 | storage = {} 16 | self._storage: StorageType = storage 17 | 18 | def add(self, item: T) -> None: 19 | item_pk = self._get_item_pk(item) 20 | self._storage[item_pk] = item 21 | self._max_pk = max(item_pk, self._max_pk) 22 | 23 | def get(self, pk: int) -> Optional[T]: 24 | return self._storage.get(pk) 25 | 26 | def search(self, **kwargs: Any) -> RamStorage[T]: 27 | def filter_by(storage, current_filter: tuple) -> StorageType: 28 | return { 29 | k: v 30 | for k, v in storage.items() 31 | if getattr(v, current_filter[0]) == current_filter[1] 32 | } 33 | 34 | storage = reduce( 35 | filter_by, [self._storage] + [(k, v) for k, v in kwargs.items()] 36 | ) 37 | return RamStorage(storage) 38 | 39 | def remove(self, item: T) -> None: 40 | pk = self._get_item_pk(item) 41 | if pk in self._storage: 42 | del (self._storage[pk]) 43 | 44 | def all(self) -> List[T]: 45 | return list(self._storage.values()) 46 | 47 | def _get_item_pk(self, item: T) -> int: 48 | return getattr(item, self._pk_name) 49 | 50 | def next_pk(self) -> int: 51 | self._max_pk += 1 52 | return self._max_pk 53 | -------------------------------------------------------------------------------- /ram_db/requirements.txt: -------------------------------------------------------------------------------- 1 | bf_shop -------------------------------------------------------------------------------- /ram_db/setup.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from setuptools import find_packages, setup 4 | 5 | lib_name = "bf_ram_db" 6 | 7 | 8 | def get_requirements(prefix: str = "") -> List[str]: 9 | return [ 10 | x.strip() for x in open("{}requirements.txt".format(prefix), "r").readlines() 11 | ] 12 | 13 | 14 | extra_requires = {"test": get_requirements(prefix="test")} 15 | 16 | setup( 17 | name=lib_name, 18 | packages=find_packages(), 19 | install_requires=get_requirements(), 20 | extra_requires=extra_requires, 21 | python_requires=">=3.7", 22 | use_scm_version={"root": "..", "relative_to": __file__}, 23 | setup_requires=["pip>=10", "setuptools>=40", "setuptools_scm"], 24 | ) -------------------------------------------------------------------------------- /ram_db/testrequirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkocjan/python_architecture/a85a1789bf1c81e12511b3cbf8953500fef79e56/ram_db/testrequirements.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup 4 | from setuptools.command.develop import develop 5 | from setuptools.command.install import install 6 | 7 | try: 8 | from pip._internal import main as pip_main 9 | except ImportError: 10 | from pip import main as pip_main 11 | 12 | PACKAGE_NAME = "the_shop" 13 | SOURCES = {"bf_shop": "shop", "bf_ram_db": "ram_db", "bf_api": "api"} 14 | 15 | 16 | def install_libs(sources, develop=False): 17 | print( 18 | "installing all libs in {} mode".format("development" if develop else "normal") 19 | ) 20 | wd = os.getcwd() 21 | 22 | for k, v in sources.items(): 23 | try: 24 | os.chdir(os.path.join(wd, v)) 25 | if develop: 26 | pip_main(["install", "-v", "-e", "."]) 27 | else: 28 | pip_main(["install", "."]) 29 | except Exception as e: 30 | print("Oops, something went wrong installing", k) 31 | print(e) 32 | finally: 33 | os.chdir(wd) 34 | 35 | 36 | class DevelopCmd(develop): 37 | """ Add custom steps for the develop command """ 38 | 39 | def run(self): 40 | develop.run(self) 41 | install_libs(SOURCES, develop=True) 42 | 43 | 44 | class InstallCmd(install): 45 | """ Add custom steps for the install command """ 46 | 47 | def run(self): 48 | install_libs(SOURCES, develop=False) 49 | install.run(self) 50 | 51 | 52 | setup( 53 | name=PACKAGE_NAME, 54 | cmdclass={"install": InstallCmd, "develop": DevelopCmd}, 55 | python_requires=">=3.7", 56 | use_scm_version={"root": ".", "relative_to": __file__}, 57 | setup_requires=["pip>=10", "setuptools>=40", "setuptools_scm"], 58 | ) 59 | -------------------------------------------------------------------------------- /shop/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E266, E501, W503 3 | max-line-length = 88 4 | max-complexity = 18 5 | select = B,C,E,F,W,T4,B9 6 | -------------------------------------------------------------------------------- /shop/.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length=88 3 | known_first_party=bf_shop 4 | known_standard_library=queue,ast,dataclasses 5 | default_section=THIRDPARTY 6 | skip=.tox 7 | multi_line_output=3 8 | include_trailing_comma=True 9 | force_grid_wrap=0 10 | combine_as_imports=True -------------------------------------------------------------------------------- /shop/bf_shop/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkocjan/python_architecture/a85a1789bf1c81e12511b3cbf8953500fef79e56/shop/bf_shop/__init__.py -------------------------------------------------------------------------------- /shop/bf_shop/entities.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from datetime import datetime 3 | from typing import List 4 | 5 | 6 | class BaseEntity: 7 | pass 8 | 9 | 10 | @dataclass(frozen=True) 11 | class Client(BaseEntity): 12 | id: int 13 | name: str 14 | 15 | 16 | @dataclass(frozen=True) 17 | class Product(BaseEntity): 18 | id: int 19 | name: str 20 | price: float 21 | 22 | 23 | @dataclass(frozen=True) 24 | class Order(BaseEntity): 25 | id: int 26 | created: datetime 27 | client: Client 28 | total_cost: float = 0 29 | items: List[Product] = field(default_factory=list) 30 | -------------------------------------------------------------------------------- /shop/bf_shop/exceptions.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | 4 | class BfException(Exception): 5 | def __init__( 6 | self, message: Optional[str] = None, detail: Optional[str] = None 7 | ) -> None: 8 | self.message = message or "" 9 | self.detail = detail or "" 10 | 11 | 12 | class ElementNotFound(BfException): 13 | NOT_FOUND = "Element not found" 14 | DETAILS = "" 15 | 16 | def __init__(self) -> None: 17 | super().__init__(message=self.NOT_FOUND, detail=self.DETAILS) 18 | 19 | 20 | class ClientNotFound(ElementNotFound): 21 | DETAILS = "Client not found" 22 | 23 | 24 | class ProductNotFound(ElementNotFound): 25 | DETAILS = "Product not found" 26 | 27 | 28 | class OrderNotFound(ElementNotFound): 29 | DETAILS = "Order not found" 30 | -------------------------------------------------------------------------------- /shop/bf_shop/logic.py: -------------------------------------------------------------------------------- 1 | from dataclasses import replace 2 | from typing import List 3 | 4 | from injector import inject 5 | 6 | from bf_shop.entities import Order 7 | from bf_shop.repositories import ClientRepository, OrderRepository, ProductRepository 8 | 9 | 10 | class OrderLogic: 11 | @inject 12 | def __init__( 13 | self, 14 | orders: OrderRepository, 15 | products: ProductRepository, 16 | clients: ClientRepository, 17 | ) -> None: 18 | self._orders: OrderRepository = orders 19 | self._products: ProductRepository = products 20 | self._clients: ClientRepository = clients 21 | 22 | def search(self, client_id: int) -> List[Order]: 23 | client = self._clients.get(client_id) 24 | return self._orders.search(client) 25 | 26 | def create(self, client_id: int) -> Order: 27 | client = self._clients.get(client_id) 28 | return self._orders.create(client=client) 29 | 30 | def add_product(self, order_id: int, product_id: int) -> Order: 31 | order = self._orders.get(order_id) 32 | product = self._products.get(product_id) 33 | 34 | order = replace( 35 | order, 36 | items=order.items + [product], 37 | total_cost=order.total_cost + product.price, 38 | ) 39 | 40 | return self._orders.save(order) 41 | -------------------------------------------------------------------------------- /shop/bf_shop/repositories.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from datetime import datetime 3 | from typing import List, Optional 4 | 5 | from bf_shop.entities import Client, Order, Product 6 | 7 | 8 | class ClientRepository(abc.ABC): 9 | @abc.abstractmethod 10 | def create(self, name: str) -> Client: 11 | pass 12 | 13 | @abc.abstractmethod 14 | def get(self, client_id: int) -> Client: 15 | pass 16 | 17 | 18 | class OrderRepository(abc.ABC): 19 | @abc.abstractmethod 20 | def create(self, client: Client) -> Order: 21 | pass 22 | 23 | @abc.abstractmethod 24 | def get(self, order_id: int) -> Order: 25 | pass 26 | 27 | @abc.abstractmethod 28 | def save(self, order: Order) -> Order: 29 | pass 30 | 31 | @abc.abstractmethod 32 | def search( 33 | self, client: Optional[Client] = None, created: Optional[datetime] = None 34 | ) -> List[Order]: 35 | pass 36 | 37 | 38 | class ProductRepository(abc.ABC): 39 | @abc.abstractmethod 40 | def get(self, product_id: int) -> Product: 41 | pass 42 | -------------------------------------------------------------------------------- /shop/mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | cache_dir=/dev/null 3 | warn_redundant_casts = true 4 | warn_unused_ignores = true 5 | ignore_missing_imports = true 6 | follow_imports = normal 7 | strict_optional = true 8 | disallow_untyped_defs = true 9 | -------------------------------------------------------------------------------- /shop/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 88 3 | py36 = true 4 | include = '\.pyi?$' 5 | exclude = ''' 6 | /( 7 | \.git 8 | | \.mypy_cache 9 | | \.eggs 10 | | \.tox 11 | | \.venv 12 | | _build 13 | | buck-out 14 | | build 15 | | dist 16 | )/ 17 | ''' 18 | -------------------------------------------------------------------------------- /shop/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs = .* caches 3 | 4 | addopts = 5 | --flake8 6 | --isort 7 | -l 8 | --cache-clear 9 | --cov-report term-missing 10 | --cov=bf_shop 11 | 12 | testpaths = bf_shop unittests 13 | -------------------------------------------------------------------------------- /shop/requirements.txt: -------------------------------------------------------------------------------- 1 | injector==0.13.4 -------------------------------------------------------------------------------- /shop/setup.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from setuptools import find_packages, setup 4 | 5 | lib_name = "bf_shop" 6 | 7 | 8 | def get_requirements(prefix: str = "") -> List[str]: 9 | return [ 10 | x.strip() for x in open("{}requirements.txt".format(prefix), "r").readlines() 11 | ] 12 | 13 | 14 | extra_requires = {"test": get_requirements(prefix="test")} 15 | 16 | setup( 17 | name=lib_name, 18 | packages=find_packages(), 19 | install_requires=get_requirements(), 20 | extra_requires=extra_requires, 21 | python_requires=">=3.7", 22 | use_scm_version={"root": "..", "relative_to": __file__}, 23 | setup_requires=["pip>=10", "setuptools>=40", "setuptools_scm"], 24 | ) 25 | -------------------------------------------------------------------------------- /shop/testrequirements.txt: -------------------------------------------------------------------------------- 1 | pytest==3.9.2 2 | pytest-flake8==1.0.4 3 | pytest-cov==2.5.1 4 | pytest-isort==0.2.0 5 | black==18.6b4 6 | isort==4.3.4 7 | mypy==0.620 -------------------------------------------------------------------------------- /shop/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37 3 | 4 | [testenv] 5 | usedevelop=True 6 | deps = 7 | -r{toxinidir}/testrequirements.txt 8 | commands = 9 | python -m pytest {posargs} 10 | black ./ --config pyproject.toml --check 11 | mypy . 12 | -------------------------------------------------------------------------------- /shop/unittests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkocjan/python_architecture/a85a1789bf1c81e12511b3cbf8953500fef79e56/shop/unittests/__init__.py -------------------------------------------------------------------------------- /shop/unittests/conftest.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List, Optional, Tuple 3 | 4 | import pytest 5 | 6 | from bf_shop.entities import Client, Order, Product 7 | from bf_shop.repositories import ClientRepository, OrderRepository, ProductRepository 8 | 9 | StaticRepositories = Tuple[OrderRepository, ProductRepository, ClientRepository] 10 | 11 | 12 | @pytest.fixture(scope="function") 13 | def prepare_repositories() -> StaticRepositories: 14 | order = Order(id=1, created=datetime.now(), client=Client(id=1, name="John Doe")) 15 | 16 | product = Product(id=1, name="Test Product", price=100) 17 | 18 | class Orders(OrderRepository): 19 | def save(self, _order: Order) -> Order: 20 | nonlocal order 21 | order = _order 22 | return order 23 | 24 | def create(self, client: Client) -> Order: 25 | ... 26 | 27 | def get(self, order_id: int) -> Order: 28 | return order 29 | 30 | def search( 31 | self, client: Optional[Client] = None, created: Optional[datetime] = None 32 | ) -> List[Order]: 33 | pass 34 | 35 | class Products(ProductRepository): 36 | def create(self, name: str, price: float) -> Product: 37 | ... 38 | 39 | def get(self, product_id: int) -> Product: 40 | return product 41 | 42 | class Clients(ClientRepository): 43 | def create(self, name: str) -> Client: 44 | ... 45 | 46 | def get(self, client_id: int) -> Client: 47 | ... 48 | 49 | return Orders(), Products(), Clients() 50 | -------------------------------------------------------------------------------- /shop/unittests/test_logic.py: -------------------------------------------------------------------------------- 1 | from unittests.conftest import StaticRepositories 2 | 3 | 4 | def test_add_product_increase_orders_total_cost( 5 | prepare_repositories: StaticRepositories 6 | ) -> None: 7 | from bf_shop.logic import OrderLogic 8 | 9 | orders, products, clients = prepare_repositories 10 | order = orders.get(1) 11 | logic = OrderLogic(orders=orders, products=products, clients=clients) 12 | 13 | assert order.total_cost == 0 14 | 15 | logic.add_product(order_id=1, product_id=1) 16 | 17 | order = orders.get(1) 18 | assert order.total_cost == 100 19 | 20 | 21 | def test_add_product_increase_orders_total_cost_minimal( 22 | prepare_repositories: StaticRepositories 23 | ) -> None: 24 | from bf_shop.logic import OrderLogic 25 | 26 | logic = OrderLogic(*prepare_repositories) 27 | 28 | order = logic.add_product(order_id=1, product_id=1) 29 | 30 | assert order.total_cost == 100 31 | --------------------------------------------------------------------------------