Compare commits
No commits in common. "3f8323d29726a5785d1895e37678b148ddb94620" and "ff8820c80a64f43b7a7f1898285d435721b13ad0" have entirely different histories.
3f8323d297
...
ff8820c80a
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,4 +8,3 @@
|
|||||||
# python
|
# python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.mypy_cache/
|
|
||||||
|
@ -23,8 +23,7 @@
|
|||||||
packages = forAllSystems (system: {
|
packages = forAllSystems (system: {
|
||||||
default = poetry2nix.${system}.mkPoetryApplication {
|
default = poetry2nix.${system}.mkPoetryApplication {
|
||||||
projectDir = self;
|
projectDir = self;
|
||||||
checkPhase = "mypy . && pytest";
|
checkPhase = "pytest";
|
||||||
preferWheels = true; # avoid long build process for `mypy`
|
|
||||||
};
|
};
|
||||||
vm = self.nixosConfigurations.vm.config.microvm.declaredRunner;
|
vm = self.nixosConfigurations.vm.config.microvm.declaredRunner;
|
||||||
});
|
});
|
||||||
|
73
poetry.lock
generated
73
poetry.lock
generated
@ -229,63 +229,6 @@ files = [
|
|||||||
{file = "ipaddress-1.0.23.tar.gz", hash = "sha256:b7f8e0369580bb4a24d5ba1d7cc29660a4a6987763faf1d8a8046830e020e7e2"},
|
{file = "ipaddress-1.0.23.tar.gz", hash = "sha256:b7f8e0369580bb4a24d5ba1d7cc29660a4a6987763faf1d8a8046830e020e7e2"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mypy"
|
|
||||||
version = "1.11.1"
|
|
||||||
description = "Optional static typing for Python"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"},
|
|
||||||
{file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"},
|
|
||||||
{file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"},
|
|
||||||
{file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"},
|
|
||||||
{file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"},
|
|
||||||
{file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"},
|
|
||||||
{file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"},
|
|
||||||
{file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"},
|
|
||||||
{file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"},
|
|
||||||
{file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"},
|
|
||||||
{file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"},
|
|
||||||
{file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"},
|
|
||||||
{file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"},
|
|
||||||
{file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"},
|
|
||||||
{file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"},
|
|
||||||
{file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"},
|
|
||||||
{file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"},
|
|
||||||
{file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"},
|
|
||||||
{file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"},
|
|
||||||
{file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"},
|
|
||||||
{file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"},
|
|
||||||
{file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"},
|
|
||||||
{file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"},
|
|
||||||
{file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"},
|
|
||||||
{file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"},
|
|
||||||
{file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"},
|
|
||||||
{file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
mypy-extensions = ">=1.0.0"
|
|
||||||
typing-extensions = ">=4.6.0"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
dmypy = ["psutil (>=4.0)"]
|
|
||||||
install-types = ["pip"]
|
|
||||||
mypyc = ["setuptools (>=50)"]
|
|
||||||
reports = ["lxml"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mypy-extensions"
|
|
||||||
version = "1.0.0"
|
|
||||||
description = "Type system extensions for programs checked with the mypy type checker."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.5"
|
|
||||||
files = [
|
|
||||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
|
||||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "24.1"
|
version = "24.1"
|
||||||
@ -504,20 +447,6 @@ anyio = ">=3.4.0,<5"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
|
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "types-requests"
|
|
||||||
version = "2.32.0.20240712"
|
|
||||||
description = "Typing stubs for requests"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "types-requests-2.32.0.20240712.tar.gz", hash = "sha256:90c079ff05e549f6bf50e02e910210b98b8ff1ebdd18e19c873cd237737c1358"},
|
|
||||||
{file = "types_requests-2.32.0.20240712-py3-none-any.whl", hash = "sha256:f754283e152c752e46e70942fa2a146b5bc70393522257bb85bd1ef7e019dcc3"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
urllib3 = ">=2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.12.2"
|
version = "4.12.2"
|
||||||
@ -567,4 +496,4 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "8c95de1de17ced501a0460661e22c77839a3afca88512e07ad95344e719679a5"
|
content-hash = "790b244c7bfa8e973a6b060852ef05929e8ca62b9be9cc5e8801fb844a15efe3"
|
||||||
|
@ -22,8 +22,6 @@ testdata = "testdata.main:main"
|
|||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
pytest = "^8.3.2"
|
pytest = "^8.3.2"
|
||||||
requests = "^2.32.3"
|
requests = "^2.32.3"
|
||||||
mypy = "^1.11.1"
|
|
||||||
types-requests = "^2.32.0.20240712"
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
38
testdata/api.py
vendored
38
testdata/api.py
vendored
@ -1,14 +1,14 @@
|
|||||||
from fastapi import FastAPI, HTTPException, status
|
from fastapi import FastAPI, HTTPException, status
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from pydantic import ValidationError
|
|
||||||
|
|
||||||
from .utils import load_database, save_database, generate_data
|
from .utils import convert_to_bytes, load_database, save_database, generate_data
|
||||||
from .custom_types import TestDataBody
|
|
||||||
|
|
||||||
def create_api(api_keys: set[str], max_size: int, max_data: int, database: str, buffer_size: int):
|
|
||||||
|
def create_api(api_keys: {str}, max_size: int, max_data: int, database: str, buffer_size: int):
|
||||||
|
|
||||||
api = FastAPI(docs_url=None, redoc_url=None)
|
api = FastAPI(docs_url=None, redoc_url=None)
|
||||||
|
|
||||||
|
|
||||||
class MaxSizePerRequestError(Exception):
|
class MaxSizePerRequestError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -20,41 +20,41 @@ def create_api(api_keys: set[str], max_size: int, max_data: int, database: str,
|
|||||||
@api.get('/')
|
@api.get('/')
|
||||||
async def test_data(api_key: str, size: str) -> StreamingResponse:
|
async def test_data(api_key: str, size: str) -> StreamingResponse:
|
||||||
try:
|
try:
|
||||||
body = TestDataBody(api_key=api_key, size=size) # type: ignore
|
if api_key not in api_keys:
|
||||||
except ValidationError as err:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail='Invalid Format.'
|
|
||||||
) from err
|
|
||||||
|
|
||||||
try:
|
|
||||||
if body.api_key not in api_keys:
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail='Invalid API Key.'
|
detail='Invalid API Key.'
|
||||||
)
|
)
|
||||||
|
|
||||||
if body.size < 0:
|
try:
|
||||||
|
size = convert_to_bytes(size)
|
||||||
|
except ValueError as err:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail='Invalid format format for size.'
|
||||||
|
) from err
|
||||||
|
|
||||||
|
if size < 0:
|
||||||
raise MinSizePerRequestError
|
raise MinSizePerRequestError
|
||||||
elif max_size < body.size:
|
elif max_size < size:
|
||||||
raise MaxSizePerRequestError
|
raise MaxSizePerRequestError
|
||||||
|
|
||||||
db = load_database(database)
|
db = load_database(database)
|
||||||
if max_data <= db['data-used'] + body.size:
|
if max_data <= db['data-used'] + size:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail='Service not available.'
|
detail='Service not available.'
|
||||||
)
|
)
|
||||||
db['data-used'] += body.size
|
db['data-used'] += size
|
||||||
|
|
||||||
save_database(database, db)
|
save_database(database, db)
|
||||||
|
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
status_code=status.HTTP_200_OK,
|
status_code=status.HTTP_200_OK,
|
||||||
content=generate_data(body.size, buffer_size),
|
content=generate_data(size, buffer_size),
|
||||||
media_type='application/octet-stream',
|
media_type='application/octet-stream',
|
||||||
headers={
|
headers={
|
||||||
'Content-Length': str(body.size)
|
'Content-Length': str(size)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
36
testdata/custom_types.py
vendored
36
testdata/custom_types.py
vendored
@ -1,36 +0,0 @@
|
|||||||
from pydantic import BaseModel, ConfigDict, field_validator
|
|
||||||
|
|
||||||
from .utils import convert_to_bytes
|
|
||||||
|
|
||||||
# class Config(BaseModel):
|
|
||||||
# host: str
|
|
||||||
# port: int
|
|
||||||
# buffer_size: int = 4 * 1024 # 4KB
|
|
||||||
# max_size: int = 2 * 1024 * 1024 * 1024 # 2GB
|
|
||||||
# max_data: int = 0 # unlimited
|
|
||||||
# api_keys = set[str]
|
|
||||||
# database = str
|
|
||||||
|
|
||||||
# model_config = ConfigDict(extra='allow')
|
|
||||||
|
|
||||||
# @field_validator('buffer_size', 'max_size', 'max_data')
|
|
||||||
# @classmethod
|
|
||||||
# def convert_size(cls, value: int | str) -> int:
|
|
||||||
# return convert_to_bytes(value)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
# class DataBase(BaseModel):
|
|
||||||
# model_config = ConfigDict(extra='forbid')
|
|
||||||
|
|
||||||
|
|
||||||
class TestDataBody(BaseModel):
|
|
||||||
api_key: str
|
|
||||||
size: int
|
|
||||||
|
|
||||||
model_config = ConfigDict(extra='forbid')
|
|
||||||
|
|
||||||
@field_validator('size', mode='before')
|
|
||||||
@classmethod
|
|
||||||
def convert_size(cls, value: str) -> int:
|
|
||||||
return convert_to_bytes(value)
|
|
18
testdata/main.py
vendored
18
testdata/main.py
vendored
@ -9,22 +9,13 @@ import uvicorn
|
|||||||
from .utils import convert_to_bytes, save_database
|
from .utils import convert_to_bytes, save_database
|
||||||
from .api import create_api
|
from .api import create_api
|
||||||
|
|
||||||
|
|
||||||
# Setup Parser
|
# Setup Parser
|
||||||
def parse_cli_arguments(argv: list[str]) -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument(
|
parser.add_argument('-c', '--config', type=argparse.FileType('r'),
|
||||||
'-c',
|
default='./config.json', help='Path to config file in JSON format.')
|
||||||
'--config',
|
|
||||||
type = argparse.FileType('r'),
|
|
||||||
default = './config.json',
|
|
||||||
help = 'Path to config file in JSON format.'
|
|
||||||
)
|
|
||||||
|
|
||||||
return parser.parse_args(argv)
|
|
||||||
|
|
||||||
|
|
||||||
def run(host: str, port: int, api_keys: set[str], max_size: int, max_data: int, database: str, buffer_size: int):
|
def run(host: str, port: int, api_keys: {str}, max_size: int, max_data: int, database: str, buffer_size: int):
|
||||||
if not exists(database) or os.stat(database).st_size == 0:
|
if not exists(database) or os.stat(database).st_size == 0:
|
||||||
save_database(database, {'data-used': 0})
|
save_database(database, {'data-used': 0})
|
||||||
|
|
||||||
@ -34,9 +25,8 @@ def run(host: str, port: int, api_keys: set[str], max_size: int, max_data: int,
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = parse_cli_arguments(sys.argv[1:])
|
args = parser.parse_args(sys.argv[1:])
|
||||||
config = json.load(args.config)
|
config = json.load(args.config)
|
||||||
|
|
||||||
host = config['host']
|
host = config['host']
|
||||||
port = config['port']
|
port = config['port']
|
||||||
buffer_size = convert_to_bytes(config['buffer-size'])
|
buffer_size = convert_to_bytes(config['buffer-size'])
|
||||||
|
19
testdata/utils.py
vendored
19
testdata/utils.py
vendored
@ -1,15 +1,10 @@
|
|||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import AsyncGenerator
|
|
||||||
|
|
||||||
|
|
||||||
def convert_to_bytes(size: int | str) -> int:
|
def convert_to_bytes(size: int | str) -> int:
|
||||||
if isinstance(size, int):
|
|
||||||
return size
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return int(size)
|
return int(size)
|
||||||
except ValueError:
|
except ValueError: # treat as string
|
||||||
units = {
|
units = {
|
||||||
'TB': 1000 ** 4, 'TiB': 1024 ** 4,
|
'TB': 1000 ** 4, 'TiB': 1024 ** 4,
|
||||||
'GB': 1000 ** 3, 'GiB': 1024 ** 3,
|
'GB': 1000 ** 3, 'GiB': 1024 ** 3,
|
||||||
@ -21,11 +16,12 @@ def convert_to_bytes(size: int | str) -> int:
|
|||||||
for unit in units:
|
for unit in units:
|
||||||
if size.endswith(unit):
|
if size.endswith(unit):
|
||||||
return int(float(size.removesuffix(unit)) * units[unit])
|
return int(float(size.removesuffix(unit)) * units[unit])
|
||||||
else:
|
break
|
||||||
raise ValueError('Invalid format. Expected integer or float ending with a data unit (B, KB, MiB,...).')
|
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
|
||||||
async def generate_data(size: int, buffer_size: int = 4 * 1024) -> AsyncGenerator[bytes, None]:
|
async def generate_data(size: int, buffer_size: int = 4 * 1024) -> bytes:
|
||||||
size_left = size
|
size_left = size
|
||||||
|
|
||||||
# https://github.com/tiangolo/fastapi/issues/5183
|
# https://github.com/tiangolo/fastapi/issues/5183
|
||||||
@ -43,6 +39,11 @@ async def generate_data(size: int, buffer_size: int = 4 * 1024) -> AsyncGenerato
|
|||||||
raise GeneratorExit
|
raise GeneratorExit
|
||||||
|
|
||||||
|
|
||||||
|
def check_policies(ip: str) -> None:
|
||||||
|
network = ipaddress.ip_network(ip)
|
||||||
|
print(network)
|
||||||
|
|
||||||
|
|
||||||
def load_database(path: str) -> dict:
|
def load_database(path: str) -> dict:
|
||||||
with open(path, 'r') as file:
|
with open(path, 'r') as file:
|
||||||
return json.load(file)
|
return json.load(file)
|
||||||
|
Loading…
Reference in New Issue
Block a user