Skip to content

Commit d2f59e1

Browse files
authored
feature: add test client for sample ecommerce service (metlo-labs#64)
1 parent c941468 commit d2f59e1

File tree

14 files changed

+380
-0
lines changed

14 files changed

+380
-0
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# syntax=docker/dockerfile:1
2+
3+
FROM python:3.8-slim-buster
4+
5+
ENV key=default_key
6+
ENV backend=default_backend
7+
ENV rps=15
8+
9+
WORKDIR /app
10+
11+
COPY requirements.txt requirements.txt
12+
RUN pip3 install -r requirements.txt
13+
14+
COPY . .
15+
16+
CMD python3 main.py -key ${key} -b ${backend} -r ${rps}

sample-service/sample-client/main.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import argparse
2+
import time
3+
import os
4+
import requests
5+
import concurrent.futures
6+
from datetime import datetime, timedelta, timezone
7+
from urllib.parse import urljoin
8+
from dotenv import load_dotenv
9+
10+
from producers.ecommerce.utils import get_products, get_carts
11+
12+
from producers import PRODUCER_CLS_MAP
13+
14+
15+
load_dotenv()
16+
17+
tick_length = timedelta(seconds=1)
18+
19+
20+
PRODUCER_MAP = {
21+
k: producer_cls(tick_length) for k, producer_cls in PRODUCER_CLS_MAP.items()
22+
}
23+
PRODUCERS = PRODUCER_MAP.values()
24+
25+
def execute_request(point, backend, api_key):
26+
path = point["path"]
27+
url = urljoin(backend, path)
28+
headers = {"x-api-key": api_key} if point["header"] else {}
29+
try:
30+
if point["method"] == "post":
31+
print
32+
body = point["body"]
33+
r = requests.post(
34+
url, json=body, headers=headers
35+
)
36+
r.raise_for_status()
37+
elif point["method"] == "get":
38+
params = point["params"]
39+
r = requests.get(
40+
url, params=params, headers=headers
41+
)
42+
r.raise_for_status()
43+
except requests.exceptions.HTTPError as err:
44+
pass
45+
46+
def run(backend, api_key, rps):
47+
while True:
48+
print("Tick")
49+
50+
current_time = datetime.now(timezone.utc)
51+
products = get_products(backend, api_key)
52+
carts = get_carts(backend, api_key)
53+
54+
for k, producer in PRODUCER_MAP.items():
55+
if not producer.should_emit(current_time):
56+
continue
57+
data_points = producer.get_data_points(current_time, products, carts, rps)
58+
if data_points:
59+
print(f"Producer {k} emitted {len(data_points)} data points.")
60+
if not data_points:
61+
continue
62+
with concurrent.futures.ThreadPoolExecutor() as executor:
63+
futures = []
64+
for point in data_points:
65+
futures.append(executor.submit(execute_request, point=point, backend=backend, api_key=api_key))
66+
67+
time.sleep(tick_length.total_seconds())
68+
69+
if __name__ == "__main__":
70+
parser = argparse.ArgumentParser()
71+
parser.add_argument("-b", "--metlo_backend", required=True)
72+
parser.add_argument("-key", "--api_key", required=True)
73+
parser.add_argument("-r", "--rps", required=True)
74+
args = parser.parse_args()
75+
metlo_backend = args.metlo_backend
76+
api_key = args.api_key
77+
rps = int(args.rps)
78+
run(metlo_backend, api_key, rps)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from producers.ecommerce.get_cart import EcommerceGetCartProducer
2+
from producers.ecommerce.get_product import EcommerceGetProductProducer
3+
from producers.ecommerce.login import EcommerceLoginProducer
4+
from producers.ecommerce.make_purchase import EcommerceMakePurchaseProducer
5+
from producers.ecommerce.new_product import EcommerceMakeProductProducer
6+
from producers.ecommerce.new_cart import EcommerceMakeCartProducer
7+
from producers.ecommerce.add_product_to_cart import EcommerceAddProductToCartProducer
8+
from producers.ecommerce.register import EcommerceRegisterProducer
9+
10+
11+
PRODUCER_CLS_MAP = {
12+
"ecommerce_add_product_to_cart": EcommerceAddProductToCartProducer,
13+
"ecommerce_make_cart": EcommerceMakeCartProducer,
14+
"ecommerce_make_product": EcommerceMakeProductProducer,
15+
"ecommerce_make_purchase": EcommerceMakePurchaseProducer,
16+
"ecommerce_login": EcommerceLoginProducer,
17+
"ecommerce_register": EcommerceRegisterProducer,
18+
"ecommerce_get_product": EcommerceGetProductProducer,
19+
"ecommerce_get_cart": EcommerceGetCartProducer,
20+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from typing import List
2+
from datetime import datetime, timedelta
3+
from random import random
4+
from faker import Faker
5+
from faker.providers import internet
6+
7+
8+
class BaseProducer:
9+
10+
avg_emit_delta = timedelta(minutes=5)
11+
12+
def __init__(self, tick_length: timedelta):
13+
self.tick_length = tick_length
14+
self.fake = Faker()
15+
self.fake.add_provider(internet)
16+
17+
def should_emit(self, time: datetime) -> bool:
18+
emit_probability = self.tick_length / self.avg_emit_delta
19+
return random() < emit_probability
20+
21+
def get_data_point(self, time: datetime) -> dict:
22+
raise NotImplementedError()
23+
24+
def get_data_points(self, time: datetime) -> List[dict]:
25+
return [self.get_data_point(time)]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from datetime import datetime, timedelta
2+
import os
3+
from typing import List
4+
from random import choice
5+
6+
from producers.base import BaseProducer
7+
8+
9+
class EcommerceAddProductToCartProducer(BaseProducer):
10+
11+
avg_emit_delta = timedelta(seconds=1)
12+
13+
def get_data_points_helper(self, products: dict, carts: dict) -> dict:
14+
product_uuid = choice(list(products.values()))["uuid"] if len(products) > 0 else None
15+
cart_uuid = choice(list(carts.values()))["uuid"] if len(carts) > 0 else None
16+
req_body = {
17+
"productUuid": product_uuid,
18+
}
19+
return {
20+
"method": "post",
21+
"path": f"/cart/{cart_uuid}/add-product",
22+
"body": req_body,
23+
"header": True
24+
}
25+
26+
def get_data_points(self, time: datetime, products=None, carts=None, rps=15) -> List[dict]:
27+
if len(products) <= 1 or len(carts) <= 1:
28+
return None
29+
return [self.get_data_points_helper(products, carts) for _ in range(rps)]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from datetime import datetime, timedelta
2+
import os
3+
from random import choice
4+
from typing import List
5+
6+
from producers.base import BaseProducer
7+
8+
9+
class EcommerceGetCartProducer(BaseProducer):
10+
11+
avg_emit_delta = timedelta(seconds=1)
12+
13+
def get_data_points_helper(self, carts: dict) -> dict:
14+
cart_uuid = choice(list(carts.values()))["uuid"] if len(carts) > 0 else ""
15+
return {
16+
"method": "get",
17+
"path": f"/cart/{cart_uuid}",
18+
"params": {},
19+
"header": True
20+
}
21+
22+
def get_data_points(self, time: datetime, products=None, carts=None, rps=15) -> List[dict]:
23+
if len(carts) <= 1:
24+
return None
25+
return [self.get_data_points_helper(carts) for _ in range(rps)]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from datetime import datetime, timedelta
2+
import os
3+
from random import choice
4+
from typing import List
5+
6+
from producers.base import BaseProducer
7+
8+
9+
class EcommerceGetProductProducer(BaseProducer):
10+
11+
avg_emit_delta = timedelta(seconds=2)
12+
13+
def get_data_points_helper(self, products: dict) -> dict:
14+
product_uuid = choice(list(products.values()))["uuid"] if len(products) > 0 else ""
15+
return {
16+
"method": "get",
17+
"path": f"/product/{product_uuid}",
18+
"params": {},
19+
"header": False
20+
}
21+
22+
def get_data_points(self, time: datetime, products=None, carts=None, rps=15) -> List[dict]:
23+
return [self.get_data_points_helper(products) for _ in range(rps)]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from datetime import datetime, timedelta
2+
from typing import List
3+
4+
from producers.base import BaseProducer
5+
6+
7+
class EcommerceLoginProducer(BaseProducer):
8+
9+
avg_emit_delta = timedelta(minutes=5)
10+
11+
def get_data_points_helper(self) -> dict:
12+
req_body = {
13+
"email": self.fake.free_email(),
14+
"password": self.fake.sentence(nb_words=5),
15+
}
16+
return {
17+
"method": "post",
18+
"path": "/login",
19+
"body": req_body,
20+
"header": False
21+
}
22+
23+
def get_data_points(self, time: datetime, products=None, carts=None, rps=8) -> List[dict]:
24+
return [self.get_data_points_helper() for _ in range(8)]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from datetime import datetime, timedelta
2+
import os
3+
from typing import List
4+
from random import choice
5+
6+
from producers.base import BaseProducer
7+
8+
9+
class EcommerceMakePurchaseProducer(BaseProducer):
10+
11+
avg_emit_delta = timedelta(seconds=1)
12+
13+
def get_data_points_helper(self, carts: dict) -> dict:
14+
cart_uuid = choice(list(carts.values()))["uuid"] if len(carts) > 0 else ""
15+
ccn = self.fake.credit_card_number()
16+
req_body = {
17+
"firstName": self.fake.first_name(),
18+
"lastName": self.fake.first_name(),
19+
"ccn": ccn,
20+
"expirationDate": self.fake.credit_card_expire(),
21+
"code": self.fake.credit_card_security_code(),
22+
"address": self.fake.address(),
23+
}
24+
return {
25+
"method": "post",
26+
"path": f"/cart/{cart_uuid}/purchase",
27+
"body": req_body,
28+
"header": True
29+
}
30+
31+
def get_data_points(self, time: datetime, products=None, carts=None, rps=15) -> List[dict]:
32+
if len(carts) <= 1:
33+
return None
34+
return [self.get_data_points_helper(carts) for _ in range(rps)]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from datetime import datetime, timedelta
2+
from typing import List
3+
4+
from producers.base import BaseProducer
5+
6+
7+
class EcommerceMakeCartProducer(BaseProducer):
8+
9+
avg_emit_delta = timedelta(seconds=1)
10+
11+
def get_data_points_helper(self) -> dict:
12+
return {
13+
"method": "post",
14+
"path": "/cart/new",
15+
"body": {},
16+
"header": True
17+
}
18+
19+
def get_data_points(self, time: datetime, products=None, carts=None, rps=8) -> List[dict]:
20+
return [self.get_data_points_helper() for _ in range(8)]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from datetime import datetime, timedelta
2+
from typing import List
3+
from random import randint
4+
5+
from producers.base import BaseProducer
6+
7+
8+
class EcommerceMakeProductProducer(BaseProducer):
9+
10+
avg_emit_delta = timedelta(seconds=1)
11+
12+
def get_data_points_helper(self) -> dict:
13+
req_body = {
14+
"name": self.fake.word(),
15+
"description": self.fake.sentence(),
16+
"warehouseAddress": self.fake.address(),
17+
"price": randint(100, 1000),
18+
}
19+
return {
20+
"method": "post",
21+
"path": "/product/new",
22+
"body": req_body,
23+
"header": True
24+
}
25+
26+
def get_data_points(self, time: datetime, products=None, carts=None, rps=8) -> List[dict]:
27+
return [self.get_data_points_helper() for _ in range(8)]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from datetime import datetime, timedelta
2+
from typing import List
3+
4+
from producers.base import BaseProducer
5+
6+
7+
class EcommerceRegisterProducer(BaseProducer):
8+
9+
avg_emit_delta = timedelta(minutes=10)
10+
11+
def get_data_points_helper(self) -> dict:
12+
req_body = {
13+
"firstName": self.fake.first_name(),
14+
"lastName": self.fake.first_name(),
15+
"email": self.fake.free_email(),
16+
"address": self.fake.address(),
17+
"phoneNumber": self.fake.phone_number(),
18+
"dob": self.fake.date_of_birth().isoformat(),
19+
"password": self.fake.sentence(nb_words=5),
20+
}
21+
return {
22+
"method": "post",
23+
"path": "/register",
24+
"body": req_body,
25+
"header": False
26+
}
27+
28+
def get_data_points(self, time: datetime, products=None, carts=None, rps=8) -> List[dict]:
29+
return [self.get_data_points_helper() for _ in range(8)]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from urllib.parse import urljoin
2+
import requests
3+
4+
def get_products(backend, api_key):
5+
try:
6+
r = requests.get(
7+
urljoin(backend, "/product"), headers={ "x-api-key": api_key }
8+
)
9+
r.raise_for_status()
10+
res = r.json()
11+
del res["ok"]
12+
return res
13+
except requests.exceptions.HTTPError as err:
14+
return {}
15+
16+
def get_carts(backend, api_key):
17+
try:
18+
r = requests.get(
19+
urljoin(backend, "/cart"), headers={ "x-api-key": api_key }
20+
)
21+
r.raise_for_status()
22+
res = r.json()
23+
del res["ok"]
24+
return res
25+
except requests.exceptions.HTTPError as err:
26+
return {}
27+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
faker
2+
requests
3+
python-dotenv

0 commit comments

Comments
 (0)