lapis-api 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lapis_api-0.2.0/.github/CONTRIBUTIONS.md +0 -0
- lapis_api-0.2.0/.github/workflows/pylint.yml +23 -0
- lapis_api-0.2.0/.github/workflows/python-publish.yml +70 -0
- lapis_api-0.2.0/.gitignore +2 -0
- lapis_api-0.2.0/.pylintrc +2 -0
- lapis_api-0.2.0/LICENSE +21 -0
- lapis_api-0.2.0/PKG-INFO +47 -0
- lapis_api-0.2.0/README.md +32 -0
- lapis_api-0.2.0/pyproject.toml +30 -0
- lapis_api-0.2.0/src/Lapis.egg-info/PKG-INFO +14 -0
- lapis_api-0.2.0/src/Lapis.egg-info/SOURCES.txt +10 -0
- lapis_api-0.2.0/src/Lapis.egg-info/dependency_links.txt +1 -0
- lapis_api-0.2.0/src/Lapis.egg-info/top_level.txt +1 -0
- lapis_api-0.2.0/src/lapis/__init__.py +10 -0
- lapis_api-0.2.0/src/lapis/lapis.py +273 -0
- lapis_api-0.2.0/src/lapis/protocols/http1.py +251 -0
- lapis_api-0.2.0/src/lapis/protocols/websocket.py +544 -0
- lapis_api-0.2.0/src/lapis/server_types.py +172 -0
- lapis_api-0.2.0/tests/basic_load_test/api/other/path.py +4 -0
- lapis_api-0.2.0/tests/basic_load_test/api/path.py +4 -0
- lapis_api-0.2.0/tests/basic_load_test/test.py +5 -0
- lapis_api-0.2.0/tests/server_config_test/config.json +4 -0
- lapis_api-0.2.0/tests/server_config_test/other/api/path.py +4 -0
- lapis_api-0.2.0/tests/server_config_test/test.py +6 -0
- lapis_api-0.2.0/tests/slugs_test/api/[slug1]/other/[slug2]/path.py +4 -0
- lapis_api-0.2.0/tests/slugs_test/api/[slug1]/path.py +4 -0
- lapis_api-0.2.0/tests/slugs_test/test.py +5 -0
- lapis_api-0.2.0/tests/streamed_response_test/api/path.py +15 -0
- lapis_api-0.2.0/tests/streamed_response_test/test.py +5 -0
- lapis_api-0.2.0/tests/websocket_test/api/path.py +15 -0
- lapis_api-0.2.0/tests/websocket_test/test.py +5 -0
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Pylint
|
|
2
|
+
|
|
3
|
+
on: [push]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
strategy:
|
|
9
|
+
matrix:
|
|
10
|
+
python-version: ["3.8", "3.9", "3.10"]
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
14
|
+
uses: actions/setup-python@v3
|
|
15
|
+
with:
|
|
16
|
+
python-version: ${{ matrix.python-version }}
|
|
17
|
+
- name: Install dependencies
|
|
18
|
+
run: |
|
|
19
|
+
python -m pip install --upgrade pip
|
|
20
|
+
pip install pylint
|
|
21
|
+
- name: Analysing the code with pylint
|
|
22
|
+
run: |
|
|
23
|
+
pylint .
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# This workflow will upload a Python Package to PyPI when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
|
3
|
+
|
|
4
|
+
# This workflow uses actions that are not certified by GitHub.
|
|
5
|
+
# They are provided by a third-party and are governed by
|
|
6
|
+
# separate terms of service, privacy policy, and support
|
|
7
|
+
# documentation.
|
|
8
|
+
|
|
9
|
+
name: Upload Python Package
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
release:
|
|
13
|
+
types: [published]
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
release-build:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: "3.x"
|
|
28
|
+
|
|
29
|
+
- name: Build release distributions
|
|
30
|
+
run: |
|
|
31
|
+
# NOTE: put your own distribution build steps here.
|
|
32
|
+
python -m pip install build
|
|
33
|
+
python -m build
|
|
34
|
+
|
|
35
|
+
- name: Upload distributions
|
|
36
|
+
uses: actions/upload-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: release-dists
|
|
39
|
+
path: dist/
|
|
40
|
+
|
|
41
|
+
pypi-publish:
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
needs:
|
|
44
|
+
- release-build
|
|
45
|
+
permissions:
|
|
46
|
+
# IMPORTANT: this permission is mandatory for trusted publishing
|
|
47
|
+
id-token: write
|
|
48
|
+
|
|
49
|
+
# Dedicated environments with protections for publishing are strongly recommended.
|
|
50
|
+
# For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
|
|
51
|
+
environment:
|
|
52
|
+
name: pypi
|
|
53
|
+
# OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
|
|
54
|
+
# url: https://pypi.org/p/YOURPROJECT
|
|
55
|
+
#
|
|
56
|
+
# ALTERNATIVE: if your GitHub Release name is the PyPI project version string
|
|
57
|
+
# ALTERNATIVE: exactly, uncomment the following line instead:
|
|
58
|
+
# url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}
|
|
59
|
+
|
|
60
|
+
steps:
|
|
61
|
+
- name: Retrieve release distributions
|
|
62
|
+
uses: actions/download-artifact@v4
|
|
63
|
+
with:
|
|
64
|
+
name: release-dists
|
|
65
|
+
path: dist/
|
|
66
|
+
|
|
67
|
+
- name: Publish release distributions to PyPI
|
|
68
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
69
|
+
with:
|
|
70
|
+
packages-dir: dist/
|
lapis_api-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [2025] [Chandler Van]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
lapis_api-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lapis-api
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: An organized way to create REST APIs
|
|
5
|
+
Project-URL: Homepage, https://github.com/CQVan/Lapis
|
|
6
|
+
Project-URL: Issues, https://github.com/CQVan/Lapis/issues
|
|
7
|
+
Author: Chandler Van
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
Lapis is a file-based REST API framework inspired by Modern Serverless Cloud Services
|
|
17
|
+
|
|
18
|
+
To create a basic Lapis server, create a folder called *api* and create python script named *path.py*
|
|
19
|
+
then within your main folder create a python script to start the server (we will call this script *main.py* in our example)
|
|
20
|
+
|
|
21
|
+
Your project directory should look like this:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
project-root/
|
|
25
|
+
|-- api/
|
|
26
|
+
| |-- path.py
|
|
27
|
+
`-- main.py
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then within the *api/path.py* file create your first GET api endpoint by adding the following code:
|
|
31
|
+
```py
|
|
32
|
+
from lapis import Response, Request
|
|
33
|
+
|
|
34
|
+
async def GET (req : Request) -> Response:
|
|
35
|
+
return Response(status_code=200, body="Hello World!")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Finally by adding the following code to *main.py* and running it:
|
|
39
|
+
```py
|
|
40
|
+
from lapis import Lapis
|
|
41
|
+
|
|
42
|
+
server = Lapis()
|
|
43
|
+
|
|
44
|
+
server.run("localhost", 80)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
You can now send an HTTP GET request to localhost:80 and recieve the famous **Hello World!** response!
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Lapis is a file-based REST API framework inspired by Modern Serverless Cloud Services
|
|
2
|
+
|
|
3
|
+
To create a basic Lapis server, create a folder called *api* and create python script named *path.py*
|
|
4
|
+
then within your main folder create a python script to start the server (we will call this script *main.py* in our example)
|
|
5
|
+
|
|
6
|
+
Your project directory should look like this:
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
project-root/
|
|
10
|
+
|-- api/
|
|
11
|
+
| |-- path.py
|
|
12
|
+
`-- main.py
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then within the *api/path.py* file create your first GET api endpoint by adding the following code:
|
|
16
|
+
```py
|
|
17
|
+
from lapis import Response, Request
|
|
18
|
+
|
|
19
|
+
async def GET (req : Request) -> Response:
|
|
20
|
+
return Response(status_code=200, body="Hello World!")
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Finally by adding the following code to *main.py* and running it:
|
|
24
|
+
```py
|
|
25
|
+
from lapis import Lapis
|
|
26
|
+
|
|
27
|
+
server = Lapis()
|
|
28
|
+
|
|
29
|
+
server.run("localhost", 80)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
You can now send an HTTP GET request to localhost:80 and recieve the famous **Hello World!** response!
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling >= 1.26"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[tool.hatch.build.targets.wheel]
|
|
6
|
+
packages = ["src/lapis"]
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "lapis-api"
|
|
10
|
+
version = "0.2.0"
|
|
11
|
+
description = "An organized way to create REST APIs"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
authors = [
|
|
15
|
+
{ name = "Chandler Van" },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
license = {text = "MIT"}
|
|
21
|
+
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/CQVan/Lapis"
|
|
30
|
+
Issues = "https://github.com/CQVan/Lapis/issues"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: Lapis
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small example package
|
|
5
|
+
Author: Chandler Van
|
|
6
|
+
Project-URL: Homepage, https://github.com/CQVan/Lapis
|
|
7
|
+
Project-URL: Issues, https://github.com/CQVan/Lapis/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE.bak
|
|
14
|
+
Dynamic: license-file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lapis
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Lapis web framework core exports.
|
|
2
|
+
|
|
3
|
+
This module exposes the primary public classes for consumers of the
|
|
4
|
+
lapis package.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .lapis import Lapis
|
|
8
|
+
from .server_types import ServerConfig, Protocol
|
|
9
|
+
from .protocols.http1 import Request, Response, StreamedResponse
|
|
10
|
+
from .protocols.websocket import WebSocketProtocol, WSPortal
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The main script for the Lapis server handling initial request handling and response
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import inspect
|
|
7
|
+
import select
|
|
8
|
+
import socket
|
|
9
|
+
import pathlib
|
|
10
|
+
import runpy
|
|
11
|
+
import sys
|
|
12
|
+
from threading import Thread
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
from lapis.protocols.websocket import WebSocketProtocol
|
|
16
|
+
from lapis.protocols.http1 import HTTP1Protocol, Request, Response
|
|
17
|
+
from .server_types import (
|
|
18
|
+
BadAPIDirectory,
|
|
19
|
+
BadConfigError,
|
|
20
|
+
BadRequest,
|
|
21
|
+
Protocol,
|
|
22
|
+
ServerConfig,
|
|
23
|
+
ProtocolEndpointError
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
class Lapis:
|
|
27
|
+
"""
|
|
28
|
+
The Lapis class implements the centeral object used to run a Lapis REST server
|
|
29
|
+
"""
|
|
30
|
+
cfg: ServerConfig = ServerConfig()
|
|
31
|
+
|
|
32
|
+
__s: socket.socket = None
|
|
33
|
+
__paths: dict = {}
|
|
34
|
+
__taken_endpoints : list[str] = []
|
|
35
|
+
__protocols : list[type[Protocol]] = []
|
|
36
|
+
|
|
37
|
+
__running : bool = False
|
|
38
|
+
|
|
39
|
+
def __init__(self, config: ServerConfig | None = None):
|
|
40
|
+
|
|
41
|
+
if config is not None:
|
|
42
|
+
self.cfg = config
|
|
43
|
+
|
|
44
|
+
self.__register_protocol(HTTP1Protocol)
|
|
45
|
+
self.__register_protocol(WebSocketProtocol)
|
|
46
|
+
|
|
47
|
+
self.__paths = self._bake_paths()
|
|
48
|
+
|
|
49
|
+
def run(self, ip: str, port: int):
|
|
50
|
+
"""
|
|
51
|
+
Starts the Lapis server to listen on a given ip and port
|
|
52
|
+
|
|
53
|
+
:param ip: The ip for the server to listen on
|
|
54
|
+
:type ip: str
|
|
55
|
+
:param port: The port for the server to listen on
|
|
56
|
+
:type port: int
|
|
57
|
+
"""
|
|
58
|
+
self.__s = socket.socket()
|
|
59
|
+
self.__s.bind((ip, port))
|
|
60
|
+
self.__s.listen()
|
|
61
|
+
|
|
62
|
+
self.__running = True
|
|
63
|
+
print(f"{self.cfg.server_name} is now listening on http://{ip}:{port}")
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
while True:
|
|
67
|
+
readable, _, _ = select.select([self.__s], [], [], 0.1)
|
|
68
|
+
if self.__s in readable:
|
|
69
|
+
client, _ = self.__s.accept()
|
|
70
|
+
t = Thread(target=self._handle_request, args=(client,), daemon=True)
|
|
71
|
+
t.start()
|
|
72
|
+
except KeyboardInterrupt:
|
|
73
|
+
pass
|
|
74
|
+
finally:
|
|
75
|
+
self.__close()
|
|
76
|
+
|
|
77
|
+
def __register_protocol(self, protocol : type[Protocol]):
|
|
78
|
+
|
|
79
|
+
if self.__running:
|
|
80
|
+
raise RuntimeError("Cannot register new Protocol while server is running")
|
|
81
|
+
|
|
82
|
+
endpoints : list[str] = protocol().get_target_endpoints()
|
|
83
|
+
if bool(set(endpoints) & set(self.__taken_endpoints)):
|
|
84
|
+
raise ProtocolEndpointError("Cannot reuse target endpoint method!")
|
|
85
|
+
|
|
86
|
+
self.__protocols.insert(0, protocol)
|
|
87
|
+
self.__taken_endpoints.extend(endpoints)
|
|
88
|
+
|
|
89
|
+
def register_protocol(self, protocol : type[Protocol]):
|
|
90
|
+
"""
|
|
91
|
+
Registers new protocol for the server to use to communicate with clients
|
|
92
|
+
|
|
93
|
+
Cannot be called while server is running
|
|
94
|
+
|
|
95
|
+
:param protocol: Description
|
|
96
|
+
:type protocol: type[Protocol]
|
|
97
|
+
"""
|
|
98
|
+
self.__register_protocol(protocol=protocol)
|
|
99
|
+
|
|
100
|
+
self.__paths = self._bake_paths()
|
|
101
|
+
|
|
102
|
+
def _get_dynamic_dirs(self, directory: pathlib.Path):
|
|
103
|
+
return [
|
|
104
|
+
p for p in directory.iterdir()
|
|
105
|
+
if p.is_dir() and p.name.startswith("[") and p.name.endswith("]")
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
def _bake_paths(self) -> dict:
|
|
109
|
+
|
|
110
|
+
server_path = pathlib.Path(sys.argv[0]).resolve()
|
|
111
|
+
root : pathlib.Path = server_path.parent / pathlib.Path(self.cfg.api_directory)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
root.parent.resolve(strict=False)
|
|
115
|
+
except (OSError, RuntimeError) as err:
|
|
116
|
+
raise BadConfigError("\"api_directory\" in config must be a valid file path") from err
|
|
117
|
+
|
|
118
|
+
if not root.exists():
|
|
119
|
+
raise BadAPIDirectory(f"api directory \"{root}\" does not exist")
|
|
120
|
+
|
|
121
|
+
result = {}
|
|
122
|
+
|
|
123
|
+
for path in root.rglob(f"{self.cfg.path_script_name}.py"):
|
|
124
|
+
if not path.is_file():
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
parts = path.relative_to(root).parts
|
|
128
|
+
current_level = result
|
|
129
|
+
current_fs_level = root
|
|
130
|
+
|
|
131
|
+
for part in parts[:-1]:
|
|
132
|
+
dynamic_dirs = self._get_dynamic_dirs(current_fs_level)
|
|
133
|
+
if len(dynamic_dirs) > 1:
|
|
134
|
+
raise BadAPIDirectory(
|
|
135
|
+
f"Multiple dynamic route folders in {current_fs_level}: "
|
|
136
|
+
f"{', '.join(d.name for d in dynamic_dirs)}"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# move filesystem pointer
|
|
140
|
+
current_fs_level = current_fs_level / part
|
|
141
|
+
|
|
142
|
+
current_level = current_level.setdefault(part, {})
|
|
143
|
+
|
|
144
|
+
script_globals = runpy.run_path(str(path.absolute()))
|
|
145
|
+
|
|
146
|
+
# Grab just endpoint methods
|
|
147
|
+
api_routes = {
|
|
148
|
+
f"/{k}": v
|
|
149
|
+
for k, v in script_globals.items()
|
|
150
|
+
if k in self.__taken_endpoints
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Add endpoints
|
|
154
|
+
current_level.update(api_routes)
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
def __has_endpoint_path(self, base_url : str) -> tuple[dict[str, any] | None, dict[str,str]]:
|
|
158
|
+
# Digs through api cache map to find the correct endpoint directory
|
|
159
|
+
slugs = {}
|
|
160
|
+
path = pathlib.Path(f"{self.cfg.api_directory}{base_url}")
|
|
161
|
+
parts : list[str] = path.relative_to(self.cfg.api_directory).parts
|
|
162
|
+
|
|
163
|
+
leaf : dict = self.__paths
|
|
164
|
+
for part in parts:
|
|
165
|
+
if part in leaf:
|
|
166
|
+
leaf = leaf[part]
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
# checks if there are dynamic routes available
|
|
170
|
+
dynamic_routes: list[str] = list(
|
|
171
|
+
{
|
|
172
|
+
key
|
|
173
|
+
for key in leaf
|
|
174
|
+
if key.startswith("[") and key.endswith("]")
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if len(dynamic_routes) == 1:
|
|
179
|
+
slugs[dynamic_routes[0].strip("[]")] = part
|
|
180
|
+
leaf = leaf[dynamic_routes[0]]
|
|
181
|
+
else:
|
|
182
|
+
return (None, {})
|
|
183
|
+
|
|
184
|
+
if len(leaf) == 0:
|
|
185
|
+
return (None, {})
|
|
186
|
+
|
|
187
|
+
return (leaf, slugs)
|
|
188
|
+
|
|
189
|
+
def _handle_request(self, client: socket.socket):
|
|
190
|
+
data = client.recv(self.cfg.max_request_size)
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
request : Request = Request(data)
|
|
194
|
+
|
|
195
|
+
except BadRequest:
|
|
196
|
+
self.__send_response(client, Response(status_code=400, body="400 Bad Request"))
|
|
197
|
+
client.close()
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
(endpoint, request.slugs) = self.__has_endpoint_path(request.base_url)
|
|
202
|
+
|
|
203
|
+
if endpoint is None:
|
|
204
|
+
raise FileNotFoundError()
|
|
205
|
+
|
|
206
|
+
# Finds the correct protocol based on the inital request
|
|
207
|
+
for protocol_cls in self.__protocols:
|
|
208
|
+
protocol: Protocol = protocol_cls()
|
|
209
|
+
|
|
210
|
+
if not protocol.identify(initial_data=data):
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
if not protocol.handshake(client=client):
|
|
214
|
+
raise BadRequest("Failed Handshake with protocol!")
|
|
215
|
+
|
|
216
|
+
target_endpoints = protocol.get_target_endpoints()
|
|
217
|
+
|
|
218
|
+
endpoints = {
|
|
219
|
+
f"/{k}": endpoint[f"/{k}"]
|
|
220
|
+
for k in target_endpoints
|
|
221
|
+
if f"/{k}" in endpoint
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
endpoints = { key.lstrip("/"): value for key, value in endpoints.items() }
|
|
225
|
+
|
|
226
|
+
if inspect.iscoroutinefunction(protocol.handle):
|
|
227
|
+
asyncio.run(protocol.handle(
|
|
228
|
+
client=client,
|
|
229
|
+
slugs=request.slugs,
|
|
230
|
+
endpoints=endpoints,
|
|
231
|
+
))
|
|
232
|
+
else:
|
|
233
|
+
protocol.handle(
|
|
234
|
+
client=client,
|
|
235
|
+
slugs=request.slugs,
|
|
236
|
+
endpoints=endpoints,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
break
|
|
240
|
+
else: # No Protocol was found to be compatible
|
|
241
|
+
raise BadRequest("No Compatible Protocol Found!")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
except BadRequest:
|
|
245
|
+
response : Response = Response(status_code=400, body="400 Bad Request")
|
|
246
|
+
self.__send_response(client=client, response=response)
|
|
247
|
+
|
|
248
|
+
except FileNotFoundError:
|
|
249
|
+
response : Response = Response(status_code=404, body="404 Not Found")
|
|
250
|
+
self.__send_response(client, response)
|
|
251
|
+
|
|
252
|
+
except RuntimeError as e:
|
|
253
|
+
print(f"Error handling request: {e}")
|
|
254
|
+
response = Response(status_code=500, body="Internal Server Error")
|
|
255
|
+
self.__send_response(client, response)
|
|
256
|
+
|
|
257
|
+
finally:
|
|
258
|
+
client.close()
|
|
259
|
+
|
|
260
|
+
def __send_response(self, client : socket.socket, response : Response):
|
|
261
|
+
client.sendall(response.to_bytes())
|
|
262
|
+
current_time = datetime.now().strftime("%H:%M:%S")
|
|
263
|
+
ip, _ = client.getpeername()
|
|
264
|
+
print(f"{current_time} {response.status_code.value} -> {ip}")
|
|
265
|
+
|
|
266
|
+
def __close(self):
|
|
267
|
+
if self.__s is not None:
|
|
268
|
+
try:
|
|
269
|
+
print("Closing Server...")
|
|
270
|
+
self.__running = False
|
|
271
|
+
self.__s.close()
|
|
272
|
+
except socket.error as e:
|
|
273
|
+
print(f"Error when closing socket: {e}")
|