autocommit 17-07-2024-07-54

This commit is contained in:
Jasen Qin 2024-07-17 07:54:02 +10:00
parent 2c201e47cd
commit 242c5c08f0
10 changed files with 645 additions and 36 deletions

534
py-kivy/API.md Normal file
View File

@ -0,0 +1,534 @@
# POS System API Documentation
## Base URL
All URLs referenced in the documentation have the following base:
```
http://localhost:8000/api/v1
```
## Authentication
Most endpoints require authentication. Use the following endpoint to obtain a JWT token:
### Login for Access Token
```
POST /token
```
**Request Body:**
```json
{
"username": "string",
"password": "string"
}
```
**Response:**
```json
{
"access_token": "string",
"token_type": "bearer"
}
```
Use the received token in the Authorization header for subsequent requests:
```
Authorization: Bearer <access_token>
```
## Items
### Create a new item
```
POST /items
```
**Request Body:**
```json
{
"name": "string",
"price": 0,
"quantity": 0,
"unit": "string",
"related_items": ["string"]
}
```
**Response:**
```json
{
"id": "string",
"name": "string",
"price": 0,
"quantity": 0,
"unit": "string",
"related_items": ["string"]
}
```
### Get all items
```
GET /items
```
**Query Parameters:**
- `skip` (optional): number of items to skip
- `limit` (optional): maximum number of items to return
**Response:**
```json
[
{
"id": "string",
"name": "string",
"price": 0,
"quantity": 0,
"unit": "string",
"related_items": ["string"]
}
]
```
### Get a specific item
```
GET /items/{item_id}
```
**Response:**
```json
{
"id": "string",
"name": "string",
"price": 0,
"quantity": 0,
"unit": "string",
"related_items": ["string"]
}
```
### Update an item
```
PUT /items/{item_id}
```
**Request Body:**
```json
{
"name": "string",
"price": 0,
"quantity": 0,
"unit": "string",
"related_items": ["string"]
}
```
**Response:**
```json
{
"id": "string",
"name": "string",
"price": 0,
"quantity": 0,
"unit": "string",
"related_items": ["string"]
}
```
### Delete an item
```
DELETE /items/{item_id}
```
**Response:**
```json
{
"message": "Item successfully deleted"
}
```
## Orders
### Create a new order
```
POST /orders
```
**Request Body:**
```json
{
"user_id": "string",
"items": [
{
"item_id": "string",
"quantity": 0,
"price_at_order": 0
}
],
"total_amount": 0,
"payment_method": "string",
"notes": "string"
}
```
**Response:**
```json
{
"id": "string",
"user_id": "string",
"items": [
{
"item_id": "string",
"quantity": 0,
"price_at_order": 0
}
],
"total_amount": 0,
"payment_method": "string",
"payment_status": "string",
"order_status": "string",
"created_at": "string",
"updated_at": "string",
"discount_applied": 0,
"notes": "string"
}
```
### Get all orders
```
GET /orders
```
**Query Parameters:**
- `skip` (optional): number of orders to skip
- `limit` (optional): maximum number of orders to return
**Response:**
```json
[
{
"id": "string",
"user_id": "string",
"items": [
{
"item_id": "string",
"quantity": 0,
"price_at_order": 0
}
],
"total_amount": 0,
"payment_method": "string",
"payment_status": "string",
"order_status": "string",
"created_at": "string",
"updated_at": "string",
"discount_applied": 0,
"notes": "string"
}
]
```
### Get a specific order
```
GET /orders/{order_id}
```
**Response:**
```json
{
"id": "string",
"user_id": "string",
"items": [
{
"item_id": "string",
"quantity": 0,
"price_at_order": 0
}
],
"total_amount": 0,
"payment_method": "string",
"payment_status": "string",
"order_status": "string",
"created_at": "string",
"updated_at": "string",
"discount_applied": 0,
"notes": "string"
}
```
### Update an order
```
PUT /orders/{order_id}
```
**Request Body:**
```json
{
"items": [
{
"item_id": "string",
"quantity": 0,
"price_at_order": 0
}
],
"total_amount": 0,
"payment_method": "string",
"payment_status": "string",
"order_status": "string",
"discount_applied": 0,
"notes": "string"
}
```
**Response:**
```json
{
"id": "string",
"user_id": "string",
"items": [
{
"item_id": "string",
"quantity": 0,
"price_at_order": 0
}
],
"total_amount": 0,
"payment_method": "string",
"payment_status": "string",
"order_status": "string",
"created_at": "string",
"updated_at": "string",
"discount_applied": 0,
"notes": "string"
}
```
### Delete an order
```
DELETE /orders/{order_id}
```
**Response:**
```json
{
"message": "Order successfully deleted"
}
```
### Process payment for an order
```
POST /orders/{order_id}/process_payment
```
**Request Body:**
```json
{
"payment_method": "string"
}
```
**Response:**
```json
{
"message": "Payment processed successfully"
}
```
### Apply discount to an order
```
POST /orders/{order_id}/apply_discount
```
**Request Body:**
```json
{
"discount_percentage": 0
}
```
**Response:**
```json
{
"id": "string",
"total_amount": 0,
"discount_applied": 0
}
```
## Users
### Register a new user
```
POST /users
```
**Request Body:**
```json
{
"username": "string",
"email": "string",
"full_name": "string",
"password": "string"
}
```
**Response:**
```json
{
"id": "string",
"username": "string",
"email": "string",
"full_name": "string",
"is_active": true,
"is_superuser": false
}
```
### Get current user
```
GET /users/me
```
**Response:**
```json
{
"id": "string",
"username": "string",
"email": "string",
"full_name": "string",
"is_active": true,
"is_superuser": false
}
```
### Get all users
```
GET /users
```
**Query Parameters:**
- `skip` (optional): number of users to skip
- `limit` (optional): maximum number of users to return
**Response:**
```json
[
{
"id": "string",
"username": "string",
"email": "string",
"full_name": "string",
"is_active": true,
"is_superuser": false
}
]
```
### Update a user
```
PUT /users/{user_id}
```
**Request Body:**
```json
{
"email": "string",
"full_name": "string",
"password": "string",
"is_active": true,
"is_superuser": false
}
```
**Response:**
```json
{
"id": "string",
"username": "string",
"email": "string",
"full_name": "string",
"is_active": true,
"is_superuser": false
}
```
### Delete a user
```
DELETE /users/{user_id}
```
**Response:**
```json
{
"message": "User successfully deleted"
}
```
## Error Responses
All endpoints can return the following error responses:
- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found
- 422 Unprocessable Entity
- 500 Internal Server Error
Error response body:
```json
{
"detail": "Error message"
}
```

36
py-kivy/poetry.lock generated
View File

@ -401,6 +401,20 @@ files = [
dnspython = ">=2.0.0" dnspython = ">=2.0.0"
idna = ">=2.0.0" idna = ">=2.0.0"
[[package]]
name = "execnet"
version = "2.1.1"
description = "execnet: rapid multi-Python deployment"
optional = false
python-versions = ">=3.8"
files = [
{file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"},
{file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"},
]
[package.extras]
testing = ["hatch", "pre-commit", "pytest", "tox"]
[[package]] [[package]]
name = "executing" name = "executing"
version = "2.0.1" version = "2.0.1"
@ -1511,6 +1525,26 @@ pluggy = ">=1.5,<2.0"
[package.extras] [package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-xdist"
version = "3.6.1"
description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"},
{file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"},
]
[package.dependencies]
execnet = ">=2.1"
pytest = ">=7.0.0"
[package.extras]
psutil = ["psutil (>=3.0)"]
setproctitle = ["setproctitle"]
testing = ["filelock"]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.9.0.post0" version = "2.9.0.post0"
@ -2308,4 +2342,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "e206ba057f9668e090364cc695646a317f0896de3d81b60e05450b7498324d84" content-hash = "0d6489d261e19e3bffaafb61a05fdc436dae4801beefcaf640e9abc4b7bad8fd"

View File

@ -1,13 +1,7 @@
from pydantic import BaseModel, Field, EmailStr, field_validator, validator, ConfigDict from pydantic import BaseModel, Field, EmailStr, field_validator, ConfigDict
from typing import List, Optional from typing import List, Optional
from datetime import datetime
from bson import ObjectId from bson import ObjectId
from datetime import datetime
def validate_object_id(value: str) -> ObjectId:
if not ObjectId.is_valid(value):
raise ValueError("Invalid ObjectId")
return ObjectId(value)
class MongoBaseModel(BaseModel): class MongoBaseModel(BaseModel):
@ -20,11 +14,12 @@ class MongoBaseModel(BaseModel):
json_encoders={ObjectId: str} json_encoders={ObjectId: str}
) )
# @field_validator("id", pre=True) @field_validator('id', mode='before')
# def validate_id(cls, v): @classmethod
# if isinstance(v, ObjectId): def convert_object_id_to_string(cls, v):
# return str(v) if isinstance(v, ObjectId):
# return v return str(v)
return v
class Item(MongoBaseModel): class Item(MongoBaseModel):
@ -40,9 +35,11 @@ class OrderItem(BaseModel):
quantity: int quantity: int
price_at_order: float price_at_order: float
# @field_validator("item_id") @field_validator("item_id")
# def validate_item_id(cls, v): def validate_id(cls, v):
# return validate_object_id(v) if isinstance(v, ObjectId):
return str(v)
return v
class Order(MongoBaseModel): class Order(MongoBaseModel):
@ -57,7 +54,7 @@ class Order(MongoBaseModel):
discount_applied: Optional[float] = None discount_applied: Optional[float] = None
notes: Optional[str] = None notes: Optional[str] = None
# @validator("user_id") # @field_validator("user_id")
# def validate_user_id(cls, v): # def validate_user_id(cls, v):
# return validate_object_id(v) # return validate_object_id(v)

View File

@ -16,14 +16,14 @@ async def create_item(item: Item):
item_dict = item.model_dump(exclude={"id"}) item_dict = item.model_dump(exclude={"id"})
result = db.items.insert_one(item_dict) result = db.items.insert_one(item_dict)
created_item = db.items.find_one({"_id": result.inserted_id}) created_item = db.items.find_one({"_id": result.inserted_id})
return Item(**created_item) return Item.model_validate(created_item)
@router.get("/", response_model=List[Item]) @router.get("/", response_model=List[Item])
async def read_items(): async def read_items():
db = get_db() db = get_db()
items = list(db.items.find()) items = list(db.items.find())
return [Item(**item) for item in items] return [Item.model_validate(item) for item in items]
@router.get("/{item_id}", response_model=Item) @router.get("/{item_id}", response_model=Item)
@ -32,7 +32,7 @@ async def read_item(item_id: str):
item = db.items.find_one({"_id": ObjectId(item_id)}) item = db.items.find_one({"_id": ObjectId(item_id)})
if item is None: if item is None:
raise HTTPException(status_code=404, detail="Item not found") raise HTTPException(status_code=404, detail="Item not found")
return Item(**item) return Item.model_validate(item)
@router.put("/{item_id}", response_model=Item) @router.put("/{item_id}", response_model=Item)
@ -44,7 +44,7 @@ async def update_item(item_id: str, item: Item):
if result.modified_count == 0: if result.modified_count == 0:
raise HTTPException(status_code=404, detail="Item not found") raise HTTPException(status_code=404, detail="Item not found")
updated_item = db.items.find_one({"_id": ObjectId(item_id)}) updated_item = db.items.find_one({"_id": ObjectId(item_id)})
return Item(**updated_item) return Item.model_validate(updated_item)
@router.delete("/{item_id}", response_model=dict) @router.delete("/{item_id}", response_model=dict)

View File

@ -17,6 +17,7 @@ python-jose = "^3.3.0"
passlib = "^1.7.4" passlib = "^1.7.4"
pydantic = "^2.8.2" pydantic = "^2.8.2"
pytest-xdist = "^3.6.1"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
ipykernel = "^6.29.5" ipykernel = "^6.29.5"
@ -30,3 +31,4 @@ indent-size = 2
[tool.poetry.scripts] [tool.poetry.scripts]
server = "pos_system.server:main" server = "pos_system.server:main"
ui = "pos_system.ui:main" ui = "pos_system.ui:main"
test-items = "tests.test_items_fastapi:test"

11
py-kivy/pytest.ini Normal file
View File

@ -0,0 +1,11 @@
# File: pytest.ini (place this in your project root directory)
[pytest]
log_cli = true
log_cli_level = INFO
log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
log_cli_date_format=%Y-%m-%d %H:%M:%S
log_file = tests/output/pytest.log
log_file_level = DEBUG
log_file_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
log_file_date_format=%Y-%m-%d %H:%M:%S

BIN
py-kivy/tests/.DS_Store vendored Normal file

Binary file not shown.

1
py-kivy/tests/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
output/

View File

View File

@ -1,11 +1,13 @@
import pytest import pytest
import logging
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from hypothesis import given, strategies as st from hypothesis import given, strategies as st, settings
from bson import ObjectId from bson import ObjectId
from pos_system.server import app from pos_system.server import app
from pos_system.database import get_db from pos_system.database import get_db
client = TestClient(app) client = TestClient(app)
logger = logging.getLogger(__name__)
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -17,15 +19,18 @@ def clear_db():
def test_create_item(): def test_create_item():
logger.info("Testing create item")
response = client.post( response = client.post(
"/items/", "/items/",
json={"name": "Test Item", "price": 10.99, "quantity": 5, "unit": "piece"} json={"name": "Test Item", "price": 10.99, "quantity": 5, "unit": "piece"}
) )
assert response.status_code == 200 assert response.status_code == 200
assert "id" in response.json() assert "_id" in response.json()
logger.info(f"Created item with ID: {response.json()['_id']}")
def test_read_items(): def test_read_items():
logger.info("Testing read items")
client.post("/items/", json={"name": "Test Item", client.post("/items/", json={"name": "Test Item",
"price": 10.99, "quantity": 5, "unit": "piece"}) "price": 10.99, "quantity": 5, "unit": "piece"})
response = client.get("/items/") response = client.get("/items/")
@ -33,54 +38,62 @@ def test_read_items():
items = response.json() items = response.json()
assert len(items) == 1 assert len(items) == 1
assert items[0]["name"] == "Test Item" assert items[0]["name"] == "Test Item"
logger.info(f"Retrieved {len(items)} items")
def test_read_item(): def test_read_item():
logger.info("Testing read single item")
create_response = client.post( create_response = client.post(
"/items/", json={"name": "Test Item", "price": 10.99, "quantity": 5, "unit": "piece"}) "/items/", json={"name": "Test Item", "price": 10.99, "quantity": 5, "unit": "piece"})
item_id = create_response.json()["id"] item_id = create_response.json()["_id"]
response = client.get(f"/items/{item_id}") response = client.get(f"/items/{item_id}")
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["name"] == "Test Item" assert response.json()["name"] == "Test Item"
logger.info(f"Retrieved item with ID: {item_id}")
def test_update_item(): def test_update_item():
logger.info("Testing update item")
create_response = client.post( create_response = client.post(
"/items/", json={"name": "Test Item", "price": 10.99, "quantity": 5, "unit": "piece"}) "/items/", json={"name": "Test Item", "price": 10.99, "quantity": 5, "unit": "piece"})
item_id = create_response.json()["id"] item_id = create_response.json()["_id"]
update_data = {"name": "Updated Item", update_data = {"name": "Updated Item",
"price": 15.99, "quantity": 10, "unit": "piece"} "price": 15.99, "quantity": 10, "unit": "piece"}
response = client.put(f"/items/{item_id}", json=update_data) response = client.put(f"/items/{item_id}", json=update_data)
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["name"] == "Updated Item" assert response.json()["name"] == "Updated Item"
logger.info(f"Updated item with ID: {item_id}")
def test_delete_item(): def test_delete_item():
logger.info("Testing delete item")
create_response = client.post( create_response = client.post(
"/items/", json={"name": "Test Item", "price": 10.99, "quantity": 5, "unit": "piece"}) "/items/", json={"name": "Test Item", "price": 10.99, "quantity": 5, "unit": "piece"})
item_id = create_response.json()["id"] item_id = create_response.json()["_id"]
response = client.delete(f"/items/{item_id}") response = client.delete(f"/items/{item_id}")
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["message"] == "Item deleted successfully" assert response.json()["message"] == "Item deleted successfully"
get_response = client.get(f"/items/{item_id}") get_response = client.get(f"/items/{item_id}")
assert get_response.status_code == 404 assert get_response.status_code == 404
logger.info(f"Deleted item with ID: {item_id}")
@given( @given(
name=st.text(min_size=1, max_size=50), name=st.text(min_size=1, max_size=20),
price=st.floats(min_value=0.01, max_value=1000000, price=st.floats(min_value=0.01, max_value=100,
allow_nan=False, allow_infinity=False), allow_nan=False, allow_infinity=False),
quantity=st.integers(min_value=0, max_value=1000000), quantity=st.integers(min_value=0, max_value=100),
unit=st.text(min_size=1, max_size=10) unit=st.text(min_size=1, max_size=10)
) )
@settings(max_examples=50) # Limit the number of examples to 50
def test_create_item_property(name, price, quantity, unit): def test_create_item_property(name, price, quantity, unit):
response = client.post( response = client.post(
"/items/", "/items/",
json={"name": name, "price": price, "quantity": quantity, "unit": unit} json={"name": name, "price": price, "quantity": quantity, "unit": unit}
) )
assert response.status_code == 200 assert response.status_code == 200
assert "id" in response.json() assert "_id" in response.json()
item_id = response.json()["id"] item_id = response.json()["_id"]
get_response = client.get(f"/items/{item_id}") get_response = client.get(f"/items/{item_id}")
assert get_response.status_code == 200 assert get_response.status_code == 200
item = get_response.json() item = get_response.json()
@ -93,15 +106,16 @@ def test_create_item_property(name, price, quantity, unit):
@given( @given(
st.lists( st.lists(
st.fixed_dictionaries({ st.fixed_dictionaries({
"name": st.text(min_size=1, max_size=50), "name": st.text(min_size=1, max_size=20),
"price": st.floats(min_value=0.01, max_value=1000000, allow_nan=False, allow_infinity=False), "price": st.floats(min_value=0.01, max_value=100, allow_nan=False, allow_infinity=False),
"quantity": st.integers(min_value=0, max_value=1000000), "quantity": st.integers(min_value=0, max_value=100),
"unit": st.text(min_size=1, max_size=10) "unit": st.text(min_size=1, max_size=10)
}), }),
min_size=1, min_size=1,
max_size=10 max_size=10
) )
) )
@settings(max_examples=20) # Limit the number of examples to 20
def test_read_items_property(items): def test_read_items_property(items):
for item in items: for item in items:
client.post("/items/", json=item) client.post("/items/", json=item)
@ -115,6 +129,22 @@ def test_read_items_property(items):
assert "quantity" in retrieved_item assert "quantity" in retrieved_item
assert "unit" in retrieved_item assert "unit" in retrieved_item
# Add this function to log only failed property tests
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
if "hypothesis" in item.keywords:
logger.error(f"Property test failed: {item.name}")
logger.error(f"Falsifying example: {call.excinfo.value}")
if __name__ == "__main__": if __name__ == "__main__":
pytest.main([__file__]) pytest.main([__file__])
def test():
pytest.main([__file__])