Compare commits
28 Commits
2025.09.17
...
2025.09.24
| Author | SHA1 | Date | |
|---|---|---|---|
| ad22bb6ecf | |||
| 01a2ff7166 | |||
| 2da6d679ad | |||
| a1cfe76047 | |||
| c71a3df62c | |||
| e296c0992b | |||
| 6639946346 | |||
| b9ac11e15f | |||
| 77d821bb6e | |||
| 86ad953ff1 | |||
| d6b7eb9b30 | |||
| 12b7c62b69 | |||
| 7fc70747bb | |||
| 6de4f3c03a | |||
| ea3055527c | |||
| 7b37748ca6 | |||
| 073b35f71a | |||
| eceff6128b | |||
| 17dfb245bf | |||
| 4f698269b5 | |||
| ddc62dfb9a | |||
| 1a67318091 | |||
| 2be58f6577 | |||
| 9c6078f4fd | |||
| dfdc31512d | |||
| ab7ff7da93 | |||
| 41087a838b | |||
| 836b92ccbf |
@@ -12,6 +12,13 @@ jobs:
|
|||||||
version_suffix: 'dev'
|
version_suffix: 'dev'
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
api:
|
||||||
|
uses: ./.gitea/workflows/package.yaml
|
||||||
|
needs: [ prepare, application, auth, core, dependency ]
|
||||||
|
with:
|
||||||
|
working_directory: src/cpl-api
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
application:
|
application:
|
||||||
uses: ./.gitea/workflows/package.yaml
|
uses: ./.gitea/workflows/package.yaml
|
||||||
needs: [ prepare, core, dependency ]
|
needs: [ prepare, core, dependency ]
|
||||||
|
|||||||
26
.gitea/workflows/test_before_merge.yaml
Normal file
26
.gitea/workflows/test_before_merge.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: Test before pr merge
|
||||||
|
run-name: Test before pr merge
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- edited
|
||||||
|
- reopened
|
||||||
|
- synchronize
|
||||||
|
- ready_for_review
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-lint:
|
||||||
|
runs-on: [ runner ]
|
||||||
|
container: git.sh-edraft.de/sh-edraft.de/act-runner:latest
|
||||||
|
steps:
|
||||||
|
- name: Clone Repository
|
||||||
|
uses: https://github.com/actions/checkout@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CI_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
- name: Installing black
|
||||||
|
run: python3.12 -m pip install black
|
||||||
|
|
||||||
|
- name: Checking black
|
||||||
|
run: python3.12 -m black src --check
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -113,6 +113,7 @@ venv.bak/
|
|||||||
|
|
||||||
# Custom Environments
|
# Custom Environments
|
||||||
cpl-env/
|
cpl-env/
|
||||||
|
.secret
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
@@ -138,3 +139,6 @@ PythonImportHelper-v2-Completion.json
|
|||||||
|
|
||||||
# cpl unittest stuff
|
# cpl unittest stuff
|
||||||
unittests/test_*_playground
|
unittests/test_*_playground
|
||||||
|
|
||||||
|
# cpl logs
|
||||||
|
**/logs/*.jsonl
|
||||||
|
|||||||
8
example/custom/api/src/appsettings.development.json
Normal file
8
example/custom/api/src/appsettings.development.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"Path": "logs/",
|
||||||
|
"Filename": "log_$start_time.log",
|
||||||
|
"ConsoleLevel": "TRACE",
|
||||||
|
"Level": "TRACE"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
{
|
{
|
||||||
"TimeFormatSettings": {
|
"TimeFormat": {
|
||||||
"DateFormat": "%Y-%m-%d",
|
"DateFormat": "%Y-%m-%d",
|
||||||
"TimeFormat": "%H:%M:%S",
|
"TimeFormat": "%H:%M:%S",
|
||||||
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
||||||
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
||||||
},
|
},
|
||||||
|
|
||||||
"LoggingSettings": {
|
"Log": {
|
||||||
"Path": "logs/",
|
"Path": "logs/",
|
||||||
"Filename": "log_$start_time.log",
|
"Filename": "log_$start_time.log",
|
||||||
"ConsoleLogLevel": "TRACE",
|
"ConsoleLevel": "TRACE",
|
||||||
"FileLogLevel": "TRACE"
|
"Level": "TRACE"
|
||||||
},
|
},
|
||||||
|
|
||||||
"DatabaseSettings": {
|
"Database": {
|
||||||
"Host": "localhost",
|
"Host": "localhost",
|
||||||
"User": "cpl",
|
"User": "cpl",
|
||||||
"Port": 3306,
|
"Port": 3306,
|
||||||
"Password": "Y3Bs",
|
"Password": "cpl",
|
||||||
"Database": "cpl",
|
"Database": "cpl",
|
||||||
"Charset": "utf8mb4",
|
"Charset": "utf8mb4",
|
||||||
"UseUnicode": "true",
|
"UseUnicode": "true",
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"TimeFormatSettings": {
|
"TimeFormat": {
|
||||||
"DateFormat": "%Y-%m-%d",
|
"DateFormat": "%Y-%m-%d",
|
||||||
"TimeFormat": "%H:%M:%S",
|
"TimeFormat": "%H:%M:%S",
|
||||||
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
||||||
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
||||||
},
|
},
|
||||||
|
|
||||||
"LoggingSettings": {
|
"Log": {
|
||||||
"Path": "logs/",
|
"Path": "logs/",
|
||||||
"Filename": "log_$start_time.log",
|
"Filename": "log_$start_time.log",
|
||||||
"ConsoleLogLevel": "ERROR",
|
"ConsoleLevel": "ERROR",
|
||||||
"FileLogLevel": "WARN"
|
"Level": "WARNING"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
47
example/custom/api/src/main.py
Normal file
47
example/custom/api/src/main.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from starlette.responses import JSONResponse
|
||||||
|
|
||||||
|
from cpl import api
|
||||||
|
from cpl.api.application.web_app import WebApp
|
||||||
|
from cpl.application import ApplicationBuilder
|
||||||
|
from cpl.auth.permission.permissions import Permissions
|
||||||
|
from cpl.auth.schema import AuthUser, Role
|
||||||
|
from cpl.core.configuration import Configuration
|
||||||
|
from cpl.core.environment import Environment
|
||||||
|
from cpl.core.utils.cache import Cache
|
||||||
|
from service import PingService
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
builder = ApplicationBuilder[WebApp](WebApp)
|
||||||
|
|
||||||
|
Configuration.add_json_file(f"appsettings.json")
|
||||||
|
Configuration.add_json_file(f"appsettings.{Environment.get_environment()}.json")
|
||||||
|
Configuration.add_json_file(f"appsettings.{Environment.get_host_name()}.json", optional=True)
|
||||||
|
|
||||||
|
# builder.services.add_logging()
|
||||||
|
builder.services.add_structured_logging()
|
||||||
|
builder.services.add_transient(PingService)
|
||||||
|
builder.services.add_module(api)
|
||||||
|
|
||||||
|
builder.services.add_cache(AuthUser)
|
||||||
|
builder.services.add_cache(Role)
|
||||||
|
|
||||||
|
app = builder.build()
|
||||||
|
app.with_logging()
|
||||||
|
app.with_database()
|
||||||
|
|
||||||
|
app.with_authentication()
|
||||||
|
app.with_authorization()
|
||||||
|
|
||||||
|
app.with_route(path="/route1", fn=lambda r: JSONResponse("route1"), method="GET", authentication=True, permissions=[Permissions.administrator])
|
||||||
|
app.with_routes_directory("routes")
|
||||||
|
|
||||||
|
provider = builder.service_provider
|
||||||
|
user_cache = provider.get_service(Cache[AuthUser])
|
||||||
|
role_cache = provider.get_service(Cache[Role])
|
||||||
|
|
||||||
|
app.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
16
example/custom/api/src/routes/ping.py
Normal file
16
example/custom/api/src/routes/ping.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from urllib.request import Request
|
||||||
|
|
||||||
|
from service import PingService
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
|
||||||
|
from cpl.api import APILogger
|
||||||
|
from cpl.api.router import Router
|
||||||
|
|
||||||
|
|
||||||
|
@Router.authenticate()
|
||||||
|
# @Router.authorize(permissions=[Permissions.administrator])
|
||||||
|
# @Router.authorize(policies=["test"])
|
||||||
|
@Router.get(f"/ping")
|
||||||
|
async def ping(r: Request, ping: PingService, logger: APILogger):
|
||||||
|
logger.info(f"Ping: {ping}")
|
||||||
|
return JSONResponse(ping.ping(r))
|
||||||
4
example/custom/api/src/service.py
Normal file
4
example/custom/api/src/service.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
class PingService:
|
||||||
|
|
||||||
|
def ping(self, r):
|
||||||
|
return "pong"
|
||||||
@@ -15,32 +15,27 @@ def test_console():
|
|||||||
Console.write_line("Hello World")
|
Console.write_line("Hello World")
|
||||||
Console.write("\nName: ")
|
Console.write("\nName: ")
|
||||||
Console.write_line(" Hello", Console.read_line())
|
Console.write_line(" Hello", Console.read_line())
|
||||||
Console.clear()
|
|
||||||
Console.write_at(5, 5, "at 5, 5")
|
Console.write_at(5, 5, "at 5, 5")
|
||||||
Console.write_at(10, 10, "at 10, 10")
|
Console.write_at(10, 10, "at 10, 10")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
Console.write_line("Hello World\n")
|
Console.write_line("Hello World\n")
|
||||||
|
Console.clear()
|
||||||
Console.spinner(
|
Console.spinner(
|
||||||
"Test:", test_spinner, spinner_foreground_color=ForegroundColorEnum.cyan, text_foreground_color="green"
|
"Test:", test_spinner, spinner_foreground_color=ForegroundColorEnum.cyan, text_foreground_color="green"
|
||||||
)
|
)
|
||||||
|
test_console()
|
||||||
Console.write_line("HOLD BACK")
|
Console.write_line("HOLD BACK")
|
||||||
# opts = [
|
opts = ["Option 1", "Option 2", "Option 3", "Option 4"]
|
||||||
# 'Option 1',
|
selected = Console.select(
|
||||||
# 'Option 2',
|
">",
|
||||||
# 'Option 3',
|
"Select item:",
|
||||||
# 'Option 4'
|
opts,
|
||||||
# ]
|
header_foreground_color=ForegroundColorEnum.blue,
|
||||||
# selected = Console.select(
|
option_foreground_color=ForegroundColorEnum.green,
|
||||||
# '>',
|
cursor_foreground_color=ForegroundColorEnum.red,
|
||||||
# 'Select item:',
|
)
|
||||||
# opts,
|
Console.write_line(f"You selected: {selected}")
|
||||||
# header_foreground_color=ForegroundColorEnum.blue,
|
|
||||||
# option_foreground_color=ForegroundColorEnum.green,
|
Console.write_line()
|
||||||
# cursor_foreground_color=ForegroundColorEnum.red
|
|
||||||
# )
|
|
||||||
# Console.write_line(f'You selected: {selected}')
|
|
||||||
# # test_console()
|
|
||||||
#
|
|
||||||
# Console.write_line()
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ProjectSettings": {
|
"Project": {
|
||||||
"Name": "database",
|
"Name": "database",
|
||||||
"Version": {
|
"Version": {
|
||||||
"Major": "0",
|
"Major": "0",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"PythonPath": {},
|
"PythonPath": {},
|
||||||
"Classifiers": []
|
"Classifiers": []
|
||||||
},
|
},
|
||||||
"BuildSettings": {
|
"Build": {
|
||||||
"ProjectType": "console",
|
"ProjectType": "console",
|
||||||
"SourcePath": "src",
|
"SourcePath": "src",
|
||||||
"OutputPath": "dist",
|
"OutputPath": "dist",
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from cpl.application.abc.application_abc import ApplicationABC
|
from cpl.application.abc import ApplicationABC
|
||||||
from cpl.auth import KeycloakAdmin
|
from cpl.auth.keycloak import KeycloakAdmin
|
||||||
from cpl.core.console import Console
|
from cpl.core.console import Console
|
||||||
from cpl.core.environment import Environment
|
from cpl.core.environment import Environment
|
||||||
from cpl.core.log import LoggerABC
|
from cpl.core.log import LoggerABC
|
||||||
@@ -14,7 +14,7 @@ class Application(ApplicationABC):
|
|||||||
def __init__(self, services: ServiceProviderABC):
|
def __init__(self, services: ServiceProviderABC):
|
||||||
ApplicationABC.__init__(self, services)
|
ApplicationABC.__init__(self, services)
|
||||||
|
|
||||||
self._logger: LoggerABC = services.get_service(LoggerABC)
|
self._logger = services.get_service(LoggerABC)
|
||||||
|
|
||||||
async def test_daos(self):
|
async def test_daos(self):
|
||||||
userDao: UserDao = self._services.get_service(UserDao)
|
userDao: UserDao = self._services.get_service(UserDao)
|
||||||
8
example/custom/database/src/appsettings.development.json
Normal file
8
example/custom/database/src/appsettings.development.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"Path": "logs/",
|
||||||
|
"Filename": "log_$start_time.log",
|
||||||
|
"ConsoleLevel": "TRACE",
|
||||||
|
"Level": "TRACE"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"TimeFormatSettings": {
|
"TimeFormat": {
|
||||||
"DateFormat": "%Y-%m-%d",
|
"DateFormat": "%Y-%m-%d",
|
||||||
"TimeFormat": "%H:%M:%S",
|
"TimeFormat": "%H:%M:%S",
|
||||||
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
||||||
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
||||||
},
|
},
|
||||||
|
|
||||||
"LoggingSettings": {
|
"Log": {
|
||||||
"Path": "logs/",
|
"Path": "logs/",
|
||||||
"Filename": "log_$start_time.log",
|
"Filename": "log_$start_time.log",
|
||||||
"ConsoleLogLevel": "TRACE",
|
"ConsoleLevel": "TRACE",
|
||||||
"FileLogLevel": "TRACE"
|
"Level": "TRACE"
|
||||||
},
|
},
|
||||||
|
|
||||||
"DatabaseSettings": {
|
"Database": {
|
||||||
"AuthPlugin": "mysql_native_password",
|
"AuthPlugin": "mysql_native_password",
|
||||||
"ConnectionString": "mysql+mysqlconnector://cpl:$credentials@localhost/cpl",
|
"ConnectionString": "mysql+mysqlconnector://cpl:$credentials@localhost/cpl",
|
||||||
"Credentials": "Y3Bs",
|
"Credentials": "Y3Bs",
|
||||||
26
example/custom/database/src/appsettings.edrafts-pc.json
Normal file
26
example/custom/database/src/appsettings.edrafts-pc.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"TimeFormat": {
|
||||||
|
"DateFormat": "%Y-%m-%d",
|
||||||
|
"TimeFormat": "%H:%M:%S",
|
||||||
|
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
||||||
|
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
||||||
|
},
|
||||||
|
|
||||||
|
"Log": {
|
||||||
|
"Path": "logs/",
|
||||||
|
"Filename": "log_$start_time.log",
|
||||||
|
"ConsoleLevel": "TRACE",
|
||||||
|
"Level": "TRACE"
|
||||||
|
},
|
||||||
|
|
||||||
|
"Database": {
|
||||||
|
"Host": "localhost",
|
||||||
|
"User": "cpl",
|
||||||
|
"Port": 3306,
|
||||||
|
"Password": "cpl",
|
||||||
|
"Database": "cpl",
|
||||||
|
"Charset": "utf8mb4",
|
||||||
|
"UseUnicode": "true",
|
||||||
|
"Buffered": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"TimeFormatSettings": {
|
"TimeFormat": {
|
||||||
"DateFormat": "%Y-%m-%d",
|
"DateFormat": "%Y-%m-%d",
|
||||||
"TimeFormat": "%H:%M:%S",
|
"TimeFormat": "%H:%M:%S",
|
||||||
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
||||||
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
||||||
},
|
},
|
||||||
|
|
||||||
"LoggingSettings": {
|
"Log": {
|
||||||
"Path": "logs/",
|
"Path": "logs/",
|
||||||
"Filename": "log_$start_time.log",
|
"Filename": "log_$start_time.log",
|
||||||
"ConsoleLogLevel": "ERROR",
|
"ConsoleLevel": "ERROR",
|
||||||
"FileLogLevel": "WARN"
|
"Level": "WARNING"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
19
example/custom/database/src/main.py
Normal file
19
example/custom/database/src/main.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from application import Application
|
||||||
|
from cpl.application import ApplicationBuilder
|
||||||
|
from custom_permissions import CustomPermissions
|
||||||
|
from startup import Startup
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
builder = ApplicationBuilder(Application).with_startup(Startup)
|
||||||
|
app = builder.build()
|
||||||
|
|
||||||
|
app.with_logging()
|
||||||
|
app.with_permissions(CustomPermissions)
|
||||||
|
app.with_migrations("./scripts")
|
||||||
|
app.with_seeders()
|
||||||
|
app.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
27
example/custom/database/src/main_simplified.py
Normal file
27
example/custom/database/src/main_simplified.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from application import Application
|
||||||
|
from cpl.application import ApplicationBuilder
|
||||||
|
from cpl.auth.permission.permissions_registry import PermissionsRegistry
|
||||||
|
from cpl.core.console import Console
|
||||||
|
from cpl.core.log import LogLevel
|
||||||
|
from custom_permissions import CustomPermissions
|
||||||
|
from startup import Startup
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
builder = ApplicationBuilder(Application).with_startup(Startup)
|
||||||
|
builder.services.add_logging()
|
||||||
|
|
||||||
|
app = builder.build()
|
||||||
|
|
||||||
|
app.with_logging(LogLevel.trace)
|
||||||
|
app.with_permissions(CustomPermissions)
|
||||||
|
app.with_migrations("./scripts")
|
||||||
|
app.with_seeders()
|
||||||
|
|
||||||
|
Console.write_line(CustomPermissions.test.value in PermissionsRegistry.get())
|
||||||
|
app.run()
|
||||||
|
Console.write_line("Hello from main_simplified.py!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -5,7 +5,7 @@ from model.city import City
|
|||||||
class CityDao(DbModelDaoABC[City]):
|
class CityDao(DbModelDaoABC[City]):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
DbModelDaoABC.__init__(self, __name__, City, "city")
|
DbModelDaoABC.__init__(self, City, "city")
|
||||||
|
|
||||||
self.attribute(City.name, str)
|
self.attribute(City.name, str)
|
||||||
self.attribute(City.zip, int)
|
self.attribute(City.zip, int)
|
||||||
@@ -5,7 +5,7 @@ from model.user import User
|
|||||||
class UserDao(DbModelDaoABC[User]):
|
class UserDao(DbModelDaoABC[User]):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
DbModelDaoABC.__init__(self, __name__, User, "users")
|
DbModelDaoABC.__init__(self, User, "users")
|
||||||
|
|
||||||
self.attribute(User.name, str)
|
self.attribute(User.name, str)
|
||||||
self.attribute(User.city_id, int, db_name="CityId")
|
self.attribute(User.city_id, int, db_name="CityId")
|
||||||
@@ -1,30 +1,26 @@
|
|||||||
from cpl import auth
|
from cpl import auth
|
||||||
from cpl.application.abc.startup_abc import StartupABC
|
from cpl.application.abc.startup_abc import StartupABC
|
||||||
from cpl.auth import permission
|
from cpl.auth import permission
|
||||||
from cpl.auth.permission.permissions_registry import PermissionsRegistry
|
|
||||||
from cpl.core.configuration import Configuration
|
from cpl.core.configuration import Configuration
|
||||||
from cpl.core.environment import Environment
|
from cpl.core.environment import Environment
|
||||||
from cpl.core.log import Logger, LoggerABC
|
from cpl.core.log import Logger, LoggerABC
|
||||||
from cpl.database import mysql
|
from cpl.database import mysql
|
||||||
from cpl.database.abc.data_access_object_abc import DataAccessObjectABC
|
from cpl.database.abc.data_access_object_abc import DataAccessObjectABC
|
||||||
from cpl.database.service.migration_service import MigrationService
|
|
||||||
from cpl.database.service.seeder_service import SeederService
|
|
||||||
from cpl.dependency import ServiceCollection
|
from cpl.dependency import ServiceCollection
|
||||||
from custom_permissions import CustomPermissions
|
|
||||||
from model.city_dao import CityDao
|
from model.city_dao import CityDao
|
||||||
from model.user_dao import UserDao
|
from model.user_dao import UserDao
|
||||||
|
|
||||||
|
|
||||||
class Startup(StartupABC):
|
class Startup(StartupABC):
|
||||||
def __init__(self):
|
|
||||||
StartupABC.__init__(self)
|
|
||||||
|
|
||||||
async def configure_configuration(self):
|
@staticmethod
|
||||||
|
async def configure_configuration():
|
||||||
Configuration.add_json_file(f"appsettings.json")
|
Configuration.add_json_file(f"appsettings.json")
|
||||||
Configuration.add_json_file(f"appsettings.{Environment.get_environment()}.json")
|
Configuration.add_json_file(f"appsettings.{Environment.get_environment()}.json")
|
||||||
Configuration.add_json_file(f"appsettings.{Environment.get_host_name()}.json", optional=True)
|
Configuration.add_json_file(f"appsettings.{Environment.get_host_name()}.json", optional=True)
|
||||||
|
|
||||||
async def configure_services(self, services: ServiceCollection):
|
@staticmethod
|
||||||
|
async def configure_services(services: ServiceCollection):
|
||||||
services.add_module(mysql)
|
services.add_module(mysql)
|
||||||
services.add_module(auth)
|
services.add_module(auth)
|
||||||
services.add_module(permission)
|
services.add_module(permission)
|
||||||
@@ -33,13 +29,3 @@ class Startup(StartupABC):
|
|||||||
services.add_transient(DataAccessObjectABC, CityDao)
|
services.add_transient(DataAccessObjectABC, CityDao)
|
||||||
|
|
||||||
services.add_singleton(LoggerABC, Logger)
|
services.add_singleton(LoggerABC, Logger)
|
||||||
|
|
||||||
PermissionsRegistry.with_enum(CustomPermissions)
|
|
||||||
|
|
||||||
provider = services.build()
|
|
||||||
migration_service: MigrationService = provider.get_service(MigrationService)
|
|
||||||
|
|
||||||
migration_service.with_directory("./scripts")
|
|
||||||
await migration_service.migrate()
|
|
||||||
seeder_service: SeederService = provider.get_service(SeederService)
|
|
||||||
await seeder_service.seed()
|
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"TimeFormatSettings": {
|
"TimeFormat": {
|
||||||
"DateFormat": "%Y-%m-%d",
|
"DateFormat": "%Y-%m-%d",
|
||||||
"TimeFormat": "%H:%M:%S",
|
"TimeFormat": "%H:%M:%S",
|
||||||
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
||||||
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
||||||
},
|
},
|
||||||
|
|
||||||
"LoggingSettings": {
|
"Logging": {
|
||||||
"Path": "logs/",
|
"Path": "logs/",
|
||||||
"Filename": "log_$start_time.log",
|
"Filename": "log_$start_time.log",
|
||||||
"ConsoleLogLevel": "ERROR",
|
"ConsoleLevel": "ERROR",
|
||||||
"FileLogLevel": "WARN"
|
"Level": "WARN"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"WorkspaceSettings": {
|
"Workspace": {
|
||||||
"DefaultProject": "di",
|
"DefaultProject": "di",
|
||||||
"Projects": {
|
"Projects": {
|
||||||
"di": "src/di/di.json"
|
"di": "src/di/di.json"
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
from cpl.application import ApplicationABC
|
from cpl.application.abc import ApplicationABC
|
||||||
from cpl.core.configuration import ConfigurationABC
|
|
||||||
from cpl.core.console.console import Console
|
from cpl.core.console.console import Console
|
||||||
from cpl.dependency import ServiceProviderABC
|
from cpl.dependency import ServiceProviderABC
|
||||||
from cpl.dependency.scope import Scope
|
from cpl.dependency.scope import Scope
|
||||||
@@ -11,15 +10,14 @@ from di.tester import Tester
|
|||||||
|
|
||||||
|
|
||||||
class Application(ApplicationABC):
|
class Application(ApplicationABC):
|
||||||
def __init__(self, config: ConfigurationABC, services: ServiceProviderABC):
|
def __init__(self, services: ServiceProviderABC):
|
||||||
ApplicationABC.__init__(self, config, services)
|
ApplicationABC.__init__(self, services)
|
||||||
|
|
||||||
def _part_of_scoped(self):
|
def _part_of_scoped(self):
|
||||||
ts: TestService = self._services.get_service(TestService)
|
ts: TestService = self._services.get_service(TestService)
|
||||||
ts.run()
|
ts.run()
|
||||||
|
|
||||||
def configure(self):
|
def configure(self): ...
|
||||||
pass
|
|
||||||
|
|
||||||
def main(self):
|
def main(self):
|
||||||
with self._services.create_scope() as scope:
|
with self._services.create_scope() as scope:
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ProjectSettings": {
|
"Project": {
|
||||||
"Name": "di",
|
"Name": "di",
|
||||||
"Version": {
|
"Version": {
|
||||||
"Major": "0",
|
"Major": "0",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"PythonPath": {},
|
"PythonPath": {},
|
||||||
"Classifiers": []
|
"Classifiers": []
|
||||||
},
|
},
|
||||||
"BuildSettings": {
|
"Build": {
|
||||||
"ProjectType": "console",
|
"ProjectType": "console",
|
||||||
"SourcePath": "",
|
"SourcePath": "",
|
||||||
"OutputPath": "../../dist",
|
"OutputPath": "../../dist",
|
||||||
@@ -6,7 +6,7 @@ from di.startup import Startup
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
app_builder = ApplicationBuilder(Application)
|
app_builder = ApplicationBuilder(Application)
|
||||||
app_builder.use_startup(Startup)
|
app_builder.with_startup(Startup)
|
||||||
app_builder.build().run()
|
app_builder.build().run()
|
||||||
|
|
||||||
|
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
from cpl.application import StartupABC
|
from cpl.application.abc import StartupABC
|
||||||
from cpl.core.configuration import ConfigurationABC
|
|
||||||
from cpl.dependency import ServiceProviderABC, ServiceCollection
|
from cpl.dependency import ServiceProviderABC, ServiceCollection
|
||||||
from cpl.core.environment import Environment
|
from di.di_tester_service import DITesterService
|
||||||
from di.test1_service import Test1Service
|
from di.test1_service import Test1Service
|
||||||
from di.test2_service import Test2Service
|
from di.test2_service import Test2Service
|
||||||
from di.test_abc import TestABC
|
from di.test_abc import TestABC
|
||||||
from di.test_service import TestService
|
from di.test_service import TestService
|
||||||
from di.di_tester_service import DITesterService
|
|
||||||
from di.tester import Tester
|
from di.tester import Tester
|
||||||
|
|
||||||
|
|
||||||
@@ -14,10 +12,9 @@ class Startup(StartupABC):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
StartupABC.__init__(self)
|
StartupABC.__init__(self)
|
||||||
|
|
||||||
def configure_configuration(self, configuration: ConfigurationABC, environment: Environment) -> ConfigurationABC:
|
def configure_configuration(self): ...
|
||||||
return configuration
|
|
||||||
|
|
||||||
def configure_services(self, services: ServiceCollection, environment: Environment) -> ServiceProviderABC:
|
def configure_services(self, services: ServiceCollection) -> ServiceProviderABC:
|
||||||
services.add_scoped(TestService)
|
services.add_scoped(TestService)
|
||||||
services.add_scoped(DITesterService)
|
services.add_scoped(DITesterService)
|
||||||
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
from cpl.core.configuration import ConfigurationABC
|
|
||||||
from cpl.dependency import ServiceProvider, ServiceProviderABC
|
from cpl.dependency import ServiceProvider, ServiceProviderABC
|
||||||
from di.test_service import TestService
|
from di.test_service import TestService
|
||||||
|
|
||||||
@@ -6,5 +5,5 @@ from di.test_service import TestService
|
|||||||
class StaticTest:
|
class StaticTest:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ServiceProvider.inject
|
@ServiceProvider.inject
|
||||||
def test(services: ServiceProviderABC, config: ConfigurationABC, t1: TestService):
|
def test(services: ServiceProviderABC, t1: TestService):
|
||||||
t1.run()
|
t1.run()
|
||||||
@@ -6,7 +6,7 @@ from cpl.core.utils.string import String
|
|||||||
|
|
||||||
class TestService:
|
class TestService:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._name = String.random_string(string.ascii_lowercase, 8)
|
self._name = String.random(8)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
Console.write_line(f"Im {self._name}")
|
Console.write_line(f"Im {self._name}")
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"Path": "logs/",
|
||||||
|
"Filename": "log_$start_time.log",
|
||||||
|
"ConsoleLevel": "TRACE",
|
||||||
|
"Level": "TRACE"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"TimeFormat": {
|
||||||
|
"DateFormat": "%Y-%m-%d",
|
||||||
|
"TimeFormat": "%H:%M:%S",
|
||||||
|
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
||||||
|
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"Path": "logs/",
|
||||||
|
"Filename": "log_$start_time.log",
|
||||||
|
"ConsoleLevel": "TRACE",
|
||||||
|
"Level": "TRACE"
|
||||||
|
},
|
||||||
|
"EMailClient": {
|
||||||
|
"Host": "mail.sh-edraft.de",
|
||||||
|
"Port": "587",
|
||||||
|
"UserName": "dev-srv@sh-edraft.de",
|
||||||
|
"Credentials": "RmBOQX1eNFYiYjgsSid3fV1nelc2WA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,26 @@
|
|||||||
{
|
{
|
||||||
"TimeFormatSettings": {
|
"TimeFormat": {
|
||||||
"DateFormat": "%Y-%m-%d",
|
"DateFormat": "%Y-%m-%d",
|
||||||
"TimeFormat": "%H:%M:%S",
|
"TimeFormat": "%H:%M:%S",
|
||||||
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
||||||
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
||||||
},
|
},
|
||||||
|
|
||||||
"LoggingSettings": {
|
"Logging": {
|
||||||
"Path": "logs/",
|
"Path": "logs/",
|
||||||
"Filename": "log_$start_time.log",
|
"Filename": "log_$start_time.log",
|
||||||
"ConsoleLogLevel": "TRACE",
|
"ConsoleLevel": "TRACE",
|
||||||
"FileLogLevel": "TRACE"
|
"Level": "TRACE"
|
||||||
},
|
},
|
||||||
|
|
||||||
"EMailClientSettings": {
|
"EMailClient": {
|
||||||
"Host": "mail.sh-edraft.de",
|
"Host": "mail.sh-edraft.de",
|
||||||
"Port": "587",
|
"Port": "587",
|
||||||
"UserName": "dev-srv@sh-edraft.de",
|
"UserName": "dev-srv@sh-edraft.de",
|
||||||
"Credentials": "RmBOQX1eNFYiYjgsSid3fV1nelc2WA=="
|
"Credentials": "RmBOQX1eNFYiYjgsSid3fV1nelc2WA=="
|
||||||
},
|
},
|
||||||
|
|
||||||
"DatabaseSettings": {
|
"Database": {
|
||||||
"Host": "localhost",
|
"Host": "localhost",
|
||||||
"User": "sh_cpl",
|
"User": "sh_cpl",
|
||||||
"Password": "MHZhc0Y2bjhKc1VUMWV0Qw==",
|
"Password": "MHZhc0Y2bjhKc1VUMWV0Qw==",
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"AuthPlugin": "mysql_native_password"
|
"AuthPlugin": "mysql_native_password"
|
||||||
},
|
},
|
||||||
|
|
||||||
"TestSettings": {
|
"Test": {
|
||||||
"Value": 20
|
"Value": 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"TimeFormatSettings": {
|
"TimeFormat": {
|
||||||
"DateFormat": "%Y-%m-%d",
|
"DateFormat": "%Y-%m-%d",
|
||||||
"TimeFormat": "%H:%M:%S",
|
"TimeFormat": "%H:%M:%S",
|
||||||
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
||||||
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
||||||
},
|
},
|
||||||
|
|
||||||
"LoggingSettings": {
|
"Logging": {
|
||||||
"Path": "logs/",
|
"Path": "logs/",
|
||||||
"Filename": "log_$start_time.log",
|
"Filename": "log_$start_time.log",
|
||||||
"ConsoleLogLevel": "ERROR",
|
"ConsoleLevel": "ERROR",
|
||||||
"FileLogLevel": "WARN"
|
"Level": "WARN"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ProjectSettings": {
|
"Project": {
|
||||||
"Name": "general",
|
"Name": "general",
|
||||||
"Version": {
|
"Version": {
|
||||||
"Major": "2021",
|
"Major": "2021",
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
},
|
},
|
||||||
"Classifiers": []
|
"Classifiers": []
|
||||||
},
|
},
|
||||||
"BuildSettings": {
|
"Build": {
|
||||||
"ProjectType": "console",
|
"ProjectType": "console",
|
||||||
"SourcePath": "",
|
"SourcePath": "",
|
||||||
"OutputPath": "dist",
|
"OutputPath": "dist",
|
||||||
@@ -7,9 +7,9 @@ from test_startup_extension import TestStartupExtension
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
app_builder = ApplicationBuilder(Application)
|
app_builder = ApplicationBuilder(Application)
|
||||||
app_builder.use_startup(Startup)
|
app_builder.with_startup(Startup)
|
||||||
app_builder.use_extension(TestStartupExtension)
|
app_builder.with_extension(TestStartupExtension)
|
||||||
app_builder.use_extension(TestExtension)
|
app_builder.with_extension(TestExtension)
|
||||||
app_builder.build().run()
|
app_builder.build().run()
|
||||||
|
|
||||||
|
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
from cpl import mail
|
from cpl import mail
|
||||||
from cpl.application.abc import StartupABC
|
from cpl.application.abc import StartupABC
|
||||||
from cpl.core.configuration import Configuration
|
from cpl.core.configuration import Configuration
|
||||||
from cpl.dependency import ServiceCollection, ServiceProviderABC
|
|
||||||
from cpl.core.environment import Environment
|
from cpl.core.environment import Environment
|
||||||
from cpl.core.pipes import IPAddressPipe
|
from cpl.core.pipes import IPAddressPipe
|
||||||
|
from cpl.dependency import ServiceCollection
|
||||||
from test_service import TestService
|
from test_service import TestService
|
||||||
|
|
||||||
|
|
||||||
class Startup(StartupABC):
|
class Startup(StartupABC):
|
||||||
def __init__(self):
|
|
||||||
StartupABC.__init__(self)
|
|
||||||
|
|
||||||
def configure_configuration(selft):
|
@staticmethod
|
||||||
|
def configure_configuration():
|
||||||
Configuration.add_json_file(f"appsettings.json")
|
Configuration.add_json_file(f"appsettings.json")
|
||||||
Configuration.add_json_file(f"appsettings.{Environment.get_environment()}.json")
|
Configuration.add_json_file(f"appsettings.{Environment.get_environment()}.json")
|
||||||
Configuration.add_json_file(f"appsettings.{Environment.get_host_name()}.json", optional=True)
|
Configuration.add_json_file(f"appsettings.{Environment.get_host_name()}.json", optional=True)
|
||||||
|
|
||||||
def configure_services(self, services: ServiceCollection):
|
@staticmethod
|
||||||
|
def configure_services(services: ServiceCollection):
|
||||||
services.add_logging()
|
services.add_logging()
|
||||||
services.add_module(mail)
|
services.add_module(mail)
|
||||||
services.add_transient(IPAddressPipe)
|
services.add_transient(IPAddressPipe)
|
||||||
@@ -4,8 +4,7 @@ from cpl.dependency import ServiceProviderABC
|
|||||||
|
|
||||||
|
|
||||||
class TestExtension(ApplicationExtensionABC):
|
class TestExtension(ApplicationExtensionABC):
|
||||||
def __init__(self):
|
|
||||||
ApplicationExtensionABC.__init__(self)
|
|
||||||
|
|
||||||
def run(self, services: ServiceProviderABC):
|
@staticmethod
|
||||||
|
def run(services: ServiceProviderABC):
|
||||||
Console.write_line("Hello World from App Extension")
|
Console.write_line("Hello World from App Extension")
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
from cpl.application.abc import StartupExtensionABC
|
from cpl.application.abc import StartupExtensionABC
|
||||||
from cpl.core.configuration import Configuration
|
|
||||||
from cpl.core.console import Console
|
from cpl.core.console import Console
|
||||||
from cpl.dependency import ServiceCollection
|
from cpl.dependency import ServiceCollection
|
||||||
from cpl.core.environment import Environment
|
|
||||||
|
|
||||||
|
|
||||||
class TestStartupExtension(StartupExtensionABC):
|
class TestStartupExtension(StartupExtensionABC):
|
||||||
def __init__(self):
|
|
||||||
StartupExtensionABC.__init__(self)
|
|
||||||
|
|
||||||
def configure_configuration(self):
|
@staticmethod
|
||||||
|
def configure_configuration():
|
||||||
Console.write_line("config")
|
Console.write_line("config")
|
||||||
|
|
||||||
def configure_services(self, services: ServiceCollection):
|
@staticmethod
|
||||||
|
def configure_services(services: ServiceCollection):
|
||||||
Console.write_line("services")
|
Console.write_line("services")
|
||||||
60
example/custom/query/main.py
Normal file
60
example/custom/query/main.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from cpl.core.console import Console
|
||||||
|
from cpl.core.utils.benchmark import Benchmark
|
||||||
|
from cpl.query.enumerable import Enumerable
|
||||||
|
from cpl.query.immutable_list import ImmutableList
|
||||||
|
from cpl.query.list import List
|
||||||
|
from cpl.query.set import Set
|
||||||
|
|
||||||
|
|
||||||
|
def _default():
|
||||||
|
Console.write_line(Enumerable.empty().to_list())
|
||||||
|
|
||||||
|
Console.write_line(Enumerable.range(0, 100).length)
|
||||||
|
Console.write_line(Enumerable.range(0, 100).to_list())
|
||||||
|
|
||||||
|
Console.write_line(Enumerable.range(0, 100).where(lambda x: x % 2 == 0).length)
|
||||||
|
Console.write_line(
|
||||||
|
Enumerable.range(0, 100).where(lambda x: x % 2 == 0).to_list().select(lambda x: str(x)).to_list()
|
||||||
|
)
|
||||||
|
Console.write_line(List)
|
||||||
|
|
||||||
|
s =Enumerable.range(0, 10).to_set()
|
||||||
|
Console.write_line(s)
|
||||||
|
s.add(1)
|
||||||
|
Console.write_line(s)
|
||||||
|
|
||||||
|
data = Enumerable(
|
||||||
|
[
|
||||||
|
{"name": "Alice", "age": 30},
|
||||||
|
{"name": "Dave", "age": 35},
|
||||||
|
{"name": "Charlie", "age": 25},
|
||||||
|
{"name": "Bob", "age": 25},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Console.write_line(data.order_by(lambda x: x["age"]).to_list())
|
||||||
|
Console.write_line(data.order_by(lambda x: x["age"]).then_by(lambda x: x["name"]).to_list())
|
||||||
|
Console.write_line(data.order_by(lambda x: x["name"]).then_by(lambda x: x["age"]).to_list())
|
||||||
|
|
||||||
|
|
||||||
|
def t_benchmark(data: list):
|
||||||
|
Benchmark.all("Enumerable", lambda: Enumerable(data).where(lambda x: x % 2 == 0).select(lambda x: x * 2).to_list())
|
||||||
|
Benchmark.all("Set", lambda: Set(data).where(lambda x: x % 2 == 0).select(lambda x: x * 2).to_list())
|
||||||
|
Benchmark.all("List", lambda: List(data).where(lambda x: x % 2 == 0).select(lambda x: x * 2).to_list())
|
||||||
|
Benchmark.all(
|
||||||
|
"ImmutableList", lambda: ImmutableList(data).where(lambda x: x % 2 == 0).select(lambda x: x * 2).to_list()
|
||||||
|
)
|
||||||
|
Benchmark.all("List comprehension", lambda: [x * 2 for x in data if x % 2 == 0])
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
N = 10_000_000
|
||||||
|
data = list(range(N))
|
||||||
|
#t_benchmark(data)
|
||||||
|
|
||||||
|
Console.write_line()
|
||||||
|
_default()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"WorkspaceSettings": {
|
"Workspace": {
|
||||||
"DefaultProject": "translation",
|
"DefaultProject": "translation",
|
||||||
"Projects": {
|
"Projects": {
|
||||||
"translation": "src/translation/translation.json"
|
"translation": "src/translation/translation.json"
|
||||||
@@ -18,8 +18,7 @@ class Application(ApplicationABC):
|
|||||||
self._translation.load_by_settings(config.get_configuration(TranslationSettings))
|
self._translation.load_by_settings(config.get_configuration(TranslationSettings))
|
||||||
self._translation.set_default_lang("de")
|
self._translation.set_default_lang("de")
|
||||||
|
|
||||||
def configure(self):
|
def configure(self): ...
|
||||||
pass
|
|
||||||
|
|
||||||
def main(self):
|
def main(self):
|
||||||
Console.write_line(self._translate.transform("main.text.hello_world"))
|
Console.write_line(self._translate.transform("main.text.hello_world"))
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"TimeFormatSettings": {
|
"TimeFormat": {
|
||||||
"DateFormat": "%Y-%m-%d",
|
"DateFormat": "%Y-%m-%d",
|
||||||
"TimeFormat": "%H:%M:%S",
|
"TimeFormat": "%H:%M:%S",
|
||||||
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
||||||
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
||||||
},
|
},
|
||||||
|
|
||||||
"LoggingSettings": {
|
"Logging": {
|
||||||
"Path": "logs/",
|
"Path": "logs/",
|
||||||
"Filename": "log_$start_time.log",
|
"Filename": "log_$start_time.log",
|
||||||
"ConsoleLogLevel": "ERROR",
|
"ConsoleLevel": "ERROR",
|
||||||
"FileLogLevel": "WARN"
|
"Level": "WARN"
|
||||||
},
|
},
|
||||||
|
|
||||||
"Translation": {
|
"Translation": {
|
||||||
@@ -6,7 +6,7 @@ from translation.startup import Startup
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
app_builder = ApplicationBuilder(Application)
|
app_builder = ApplicationBuilder(Application)
|
||||||
app_builder.use_startup(Startup)
|
app_builder.with_startup(Startup)
|
||||||
app_builder.build().run()
|
app_builder.build().run()
|
||||||
|
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ProjectSettings": {
|
"Project": {
|
||||||
"Name": "translation",
|
"Name": "translation",
|
||||||
"Version": {
|
"Version": {
|
||||||
"Major": "0",
|
"Major": "0",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"PythonPath": {},
|
"PythonPath": {},
|
||||||
"Classifiers": []
|
"Classifiers": []
|
||||||
},
|
},
|
||||||
"BuildSettings": {
|
"Build": {
|
||||||
"ProjectType": "console",
|
"ProjectType": "console",
|
||||||
"SourcePath": "",
|
"SourcePath": "",
|
||||||
"OutputPath": "../../dist",
|
"OutputPath": "../../dist",
|
||||||
61
install.sh
Normal file
61
install.sh
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Find and combine requirements from src/cpl-*/requirements.txt,
|
||||||
|
# filtering out lines whose *package name* starts with "cpl-".
|
||||||
|
# Works with pinned versions, extras, markers, editable installs, and VCS refs.
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
|
||||||
|
req_files=(src/cpl-*/requirements.txt)
|
||||||
|
if ((${#req_files[@]} == 0)); then
|
||||||
|
echo "No requirements files found at src/cpl-*/requirements.txt" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp_combined="$(mktemp)"
|
||||||
|
trap 'rm -f "$tmp_combined"' EXIT
|
||||||
|
|
||||||
|
# Concatenate, trim comments/whitespace, filter out cpl-* packages, dedupe.
|
||||||
|
# We keep non-package options/flags/constraints as-is.
|
||||||
|
awk '
|
||||||
|
function trim(s){ sub(/^[[:space:]]+/,"",s); sub(/[[:space:]]+$/,"",s); return s }
|
||||||
|
|
||||||
|
{
|
||||||
|
line=$0
|
||||||
|
# drop full-line comments and strip inline comments
|
||||||
|
if (line ~ /^[[:space:]]*#/) next
|
||||||
|
sub(/#[^!].*$/,"",line) # strip trailing comment (simple heuristic)
|
||||||
|
line=trim(line)
|
||||||
|
if (line == "") next
|
||||||
|
|
||||||
|
# Determine the package *name* even for "-e", extras, pins, markers, or VCS "@"
|
||||||
|
e = line
|
||||||
|
sub(/^-e[[:space:]]+/,"",e) # remove editable prefix
|
||||||
|
# Tokenize up to the first of these separators: space, [ < > = ! ~ ; @
|
||||||
|
token = e
|
||||||
|
sub(/\[.*/,"",token) # remove extras quickly
|
||||||
|
n = split(token, a, /[<>=!~;@[:space:]]/)
|
||||||
|
name = tolower(a[1])
|
||||||
|
|
||||||
|
# If the first token (name) starts with "cpl-", skip this requirement
|
||||||
|
if (name ~ /^cpl-/) next
|
||||||
|
|
||||||
|
print line
|
||||||
|
}
|
||||||
|
' "${req_files[@]}" | sort -u > "$tmp_combined"
|
||||||
|
|
||||||
|
if ! [ -s "$tmp_combined" ]; then
|
||||||
|
echo "Nothing to install after filtering out cpl-* packages." >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Installing dependencies (excluding cpl-*) from:"
|
||||||
|
printf ' - %s\n' "${req_files[@]}"
|
||||||
|
echo
|
||||||
|
echo "Final set to install:"
|
||||||
|
cat "$tmp_combined"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Use python -m pip for reliability; change to python3 if needed.
|
||||||
|
python -m pip install -r "$tmp_combined"
|
||||||
36
src/cpl-api/cpl/api/__init__.py
Normal file
36
src/cpl-api/cpl/api/__init__.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from cpl.dependency.service_collection import ServiceCollection as _ServiceCollection
|
||||||
|
|
||||||
|
from .error import APIError, AlreadyExists, EndpointNotImplemented, Forbidden, NotFound, Unauthorized
|
||||||
|
from .logger import APILogger
|
||||||
|
from .settings import ApiSettings
|
||||||
|
|
||||||
|
|
||||||
|
def add_api(collection: _ServiceCollection):
|
||||||
|
try:
|
||||||
|
from cpl.database import mysql
|
||||||
|
|
||||||
|
collection.add_module(mysql)
|
||||||
|
except ImportError as e:
|
||||||
|
from cpl.core.errors import dependency_error
|
||||||
|
|
||||||
|
dependency_error("cpl-database", e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cpl import auth
|
||||||
|
from cpl.auth import permission
|
||||||
|
|
||||||
|
collection.add_module(auth)
|
||||||
|
collection.add_module(permission)
|
||||||
|
except ImportError as e:
|
||||||
|
from cpl.core.errors import dependency_error
|
||||||
|
|
||||||
|
dependency_error("cpl-auth", e)
|
||||||
|
|
||||||
|
from cpl.api.registry.policy import PolicyRegistry
|
||||||
|
from cpl.api.registry.route import RouteRegistry
|
||||||
|
|
||||||
|
collection.add_singleton(PolicyRegistry)
|
||||||
|
collection.add_singleton(RouteRegistry)
|
||||||
|
|
||||||
|
|
||||||
|
_ServiceCollection.with_module(add_api, __name__)
|
||||||
1
src/cpl-api/cpl/api/abc/__init__.py
Normal file
1
src/cpl-api/cpl/api/abc/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .asgi_middleware_abc import ASGIMiddleware
|
||||||
15
src/cpl-api/cpl/api/abc/asgi_middleware_abc.py
Normal file
15
src/cpl-api/cpl/api/abc/asgi_middleware_abc.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from starlette.types import Scope, Receive, Send
|
||||||
|
|
||||||
|
|
||||||
|
class ASGIMiddleware(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def __init__(self, app):
|
||||||
|
self._app = app
|
||||||
|
|
||||||
|
def _call_next(self, scope: Scope, receive: Receive, send: Send):
|
||||||
|
return self._app(scope, receive, send)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def __call__(self, scope: Scope, receive: Receive, send: Send): ...
|
||||||
1
src/cpl-api/cpl/api/application/__init__.py
Normal file
1
src/cpl-api/cpl/api/application/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .web_app import WebApp
|
||||||
249
src/cpl-api/cpl/api/application/web_app.py
Normal file
249
src/cpl-api/cpl/api/application/web_app.py
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import os
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Mapping, Any, Callable, Self, Union
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
from starlette.applications import Starlette
|
||||||
|
from starlette.middleware import Middleware
|
||||||
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
from starlette.types import ExceptionHandler
|
||||||
|
|
||||||
|
from cpl import api, auth
|
||||||
|
from cpl.api.error import APIError
|
||||||
|
from cpl.api.logger import APILogger
|
||||||
|
from cpl.api.middleware.authentication import AuthenticationMiddleware
|
||||||
|
from cpl.api.middleware.authorization import AuthorizationMiddleware
|
||||||
|
from cpl.api.middleware.logging import LoggingMiddleware
|
||||||
|
from cpl.api.middleware.request import RequestMiddleware
|
||||||
|
from cpl.api.model.api_route import ApiRoute
|
||||||
|
from cpl.api.model.policy import Policy
|
||||||
|
from cpl.api.model.validation_match import ValidationMatch
|
||||||
|
from cpl.api.registry.policy import PolicyRegistry
|
||||||
|
from cpl.api.registry.route import RouteRegistry
|
||||||
|
from cpl.api.router import Router
|
||||||
|
from cpl.api.settings import ApiSettings
|
||||||
|
from cpl.api.typing import HTTPMethods, PartialMiddleware, PolicyResolver
|
||||||
|
from cpl.application.abc.application_abc import ApplicationABC
|
||||||
|
from cpl.core.configuration import Configuration
|
||||||
|
from cpl.dependency.service_provider_abc import ServiceProviderABC
|
||||||
|
|
||||||
|
|
||||||
|
PolicyInput = Union[dict[str, PolicyResolver], Policy]
|
||||||
|
|
||||||
|
|
||||||
|
class WebApp(ApplicationABC):
|
||||||
|
def __init__(self, services: ServiceProviderABC):
|
||||||
|
super().__init__(services, [auth, api])
|
||||||
|
self._app: Starlette | None = None
|
||||||
|
|
||||||
|
self._logger = services.get_service(APILogger)
|
||||||
|
|
||||||
|
self._api_settings = Configuration.get(ApiSettings)
|
||||||
|
self._policies = services.get_service(PolicyRegistry)
|
||||||
|
self._routes = services.get_service(RouteRegistry)
|
||||||
|
|
||||||
|
self._middleware: list[Middleware] = [
|
||||||
|
Middleware(RequestMiddleware),
|
||||||
|
Middleware(LoggingMiddleware),
|
||||||
|
]
|
||||||
|
self._exception_handlers: Mapping[Any, ExceptionHandler] = {
|
||||||
|
Exception: self._handle_exception,
|
||||||
|
APIError: self._handle_exception,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _handle_exception(self, request: Request, exc: Exception):
|
||||||
|
if isinstance(exc, APIError):
|
||||||
|
self._logger.error(exc)
|
||||||
|
return JSONResponse({"error": str(exc)}, status_code=exc.status_code)
|
||||||
|
|
||||||
|
if hasattr(request.state, "request_id"):
|
||||||
|
self._logger.error(f"Request {request.state.request_id}", exc)
|
||||||
|
else:
|
||||||
|
self._logger.error("Request unknown", exc)
|
||||||
|
|
||||||
|
return JSONResponse({"error": str(exc)}, status_code=500)
|
||||||
|
|
||||||
|
def _get_allowed_origins(self):
|
||||||
|
origins = self._api_settings.allowed_origins
|
||||||
|
|
||||||
|
if origins is None or origins == "":
|
||||||
|
self._logger.warning("No allowed origins specified, allowing all origins")
|
||||||
|
return ["*"]
|
||||||
|
|
||||||
|
self._logger.debug(f"Allowed origins: {origins}")
|
||||||
|
return origins.split(",")
|
||||||
|
|
||||||
|
def with_database(self) -> Self:
|
||||||
|
self.with_migrations()
|
||||||
|
self.with_seeders()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def with_app(self, app: Starlette) -> Self:
|
||||||
|
assert app is not None, "app must not be None"
|
||||||
|
assert isinstance(app, Starlette), "app must be an instance of Starlette"
|
||||||
|
self._app = app
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _check_for_app(self):
|
||||||
|
if self._app is not None:
|
||||||
|
raise ValueError("App is already set, cannot add routes or middleware")
|
||||||
|
|
||||||
|
def with_routes_directory(self, directory: str) -> Self:
|
||||||
|
self._check_for_app()
|
||||||
|
assert directory is not None, "directory must not be None"
|
||||||
|
|
||||||
|
base = directory.replace("/", ".").replace("\\", ".")
|
||||||
|
|
||||||
|
for filename in os.listdir(directory):
|
||||||
|
if not filename.endswith(".py") or filename == "__init__.py":
|
||||||
|
continue
|
||||||
|
|
||||||
|
__import__(f"{base}.{filename[:-3]}")
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def with_routes(
|
||||||
|
self,
|
||||||
|
routes: list[ApiRoute],
|
||||||
|
method: HTTPMethods,
|
||||||
|
authentication: bool = False,
|
||||||
|
roles: list[str | Enum] = None,
|
||||||
|
permissions: list[str | Enum] = None,
|
||||||
|
policies: list[str] = None,
|
||||||
|
match: ValidationMatch = None,
|
||||||
|
) -> Self:
|
||||||
|
self._check_for_app()
|
||||||
|
assert self._routes is not None, "routes must not be None"
|
||||||
|
assert all(isinstance(route, ApiRoute) for route in routes), "all routes must be of type ApiRoute"
|
||||||
|
for route in routes:
|
||||||
|
self.with_route(
|
||||||
|
route.path,
|
||||||
|
route.fn,
|
||||||
|
method,
|
||||||
|
authentication,
|
||||||
|
roles,
|
||||||
|
permissions,
|
||||||
|
policies,
|
||||||
|
match,
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def with_route(
|
||||||
|
self,
|
||||||
|
path: str,
|
||||||
|
fn: Callable[[Request], Any],
|
||||||
|
method: HTTPMethods,
|
||||||
|
authentication: bool = False,
|
||||||
|
roles: list[str | Enum] = None,
|
||||||
|
permissions: list[str | Enum] = None,
|
||||||
|
policies: list[str] = None,
|
||||||
|
match: ValidationMatch = None,
|
||||||
|
) -> Self:
|
||||||
|
self._check_for_app()
|
||||||
|
assert path is not None, "path must not be None"
|
||||||
|
assert fn is not None, "fn must not be None"
|
||||||
|
assert method in [
|
||||||
|
"GET",
|
||||||
|
"HEAD",
|
||||||
|
"POST",
|
||||||
|
"PUT",
|
||||||
|
"PATCH",
|
||||||
|
"DELETE",
|
||||||
|
"OPTIONS",
|
||||||
|
], "method must be a valid HTTP method"
|
||||||
|
|
||||||
|
Router.route(path, method, registry=self._routes)(fn)
|
||||||
|
|
||||||
|
if authentication:
|
||||||
|
Router.authenticate()(fn)
|
||||||
|
|
||||||
|
if roles or permissions or policies:
|
||||||
|
Router.authorize(roles, permissions, policies, match)(fn)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def with_middleware(self, middleware: PartialMiddleware) -> Self:
|
||||||
|
self._check_for_app()
|
||||||
|
|
||||||
|
if isinstance(middleware, Middleware):
|
||||||
|
self._middleware.append(middleware)
|
||||||
|
elif callable(middleware):
|
||||||
|
self._middleware.append(Middleware(middleware))
|
||||||
|
else:
|
||||||
|
raise ValueError("middleware must be of type starlette.middleware.Middleware or a callable")
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def with_authentication(self) -> Self:
|
||||||
|
self.with_middleware(AuthenticationMiddleware)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def with_authorization(self, *policies: list[PolicyInput] | PolicyInput) -> Self:
|
||||||
|
if policies:
|
||||||
|
_policies = []
|
||||||
|
|
||||||
|
if not isinstance(policies, list):
|
||||||
|
policies = list(policies)
|
||||||
|
|
||||||
|
for i, policy in enumerate(policies):
|
||||||
|
if isinstance(policy, dict):
|
||||||
|
for name, resolver in policy.items():
|
||||||
|
if not isinstance(name, str):
|
||||||
|
self._logger.warning(f"Skipping policy at index {i}, name must be a string")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not callable(resolver):
|
||||||
|
self._logger.warning(f"Skipping policy {name}, resolver must be callable")
|
||||||
|
continue
|
||||||
|
|
||||||
|
_policies.append(Policy(name, resolver))
|
||||||
|
continue
|
||||||
|
|
||||||
|
_policies.append(policy)
|
||||||
|
|
||||||
|
self._policies.extend(_policies)
|
||||||
|
|
||||||
|
self.with_middleware(AuthorizationMiddleware)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _validate_policies(self):
|
||||||
|
for rule in Router.get_authorization_rules():
|
||||||
|
for policy_name in rule["policies"]:
|
||||||
|
policy = self._policies.get(policy_name)
|
||||||
|
if not policy:
|
||||||
|
self._logger.fatal(f"Authorization policy '{policy_name}' not found")
|
||||||
|
|
||||||
|
async def main(self):
|
||||||
|
self._logger.debug(f"Preparing API")
|
||||||
|
self._validate_policies()
|
||||||
|
|
||||||
|
if self._app is None:
|
||||||
|
routes = [route.to_starlette(self._services.inject) for route in self._routes.all()]
|
||||||
|
|
||||||
|
app = Starlette(
|
||||||
|
routes=routes,
|
||||||
|
middleware=[
|
||||||
|
*self._middleware,
|
||||||
|
Middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=self._get_allowed_origins(),
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
exception_handlers=self._exception_handlers,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
app = self._app
|
||||||
|
|
||||||
|
self._logger.info(f"Start API on {self._api_settings.host}:{self._api_settings.port}")
|
||||||
|
|
||||||
|
config = uvicorn.Config(
|
||||||
|
app, host=self._api_settings.host, port=self._api_settings.port, log_config=None, loop="asyncio"
|
||||||
|
)
|
||||||
|
server = uvicorn.Server(config)
|
||||||
|
await server.serve()
|
||||||
|
|
||||||
|
self._logger.info("Shutdown API")
|
||||||
46
src/cpl-api/cpl/api/error.py
Normal file
46
src/cpl-api/cpl/api/error.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
from http.client import HTTPException
|
||||||
|
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
from starlette.types import Scope, Receive, Send
|
||||||
|
|
||||||
|
|
||||||
|
class APIError(HTTPException):
|
||||||
|
status_code = 500
|
||||||
|
|
||||||
|
def __init__(self, message: str = ""):
|
||||||
|
super().__init__(self.status_code, message)
|
||||||
|
self._message = message
|
||||||
|
|
||||||
|
@property
|
||||||
|
def error_message(self) -> str:
|
||||||
|
if self._message:
|
||||||
|
return f"{type(self).__name__}: {self._message}"
|
||||||
|
|
||||||
|
return f"{type(self).__name__}"
|
||||||
|
|
||||||
|
async def asgi_response(self, scope: Scope, receive: Receive, send: Send):
|
||||||
|
r = JSONResponse({"error": self.error_message}, status_code=self.status_code)
|
||||||
|
return await r(scope, receive, send)
|
||||||
|
|
||||||
|
def response(self):
|
||||||
|
return JSONResponse({"error": self.error_message}, status_code=self.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
class Unauthorized(APIError):
|
||||||
|
status_code = 401
|
||||||
|
|
||||||
|
|
||||||
|
class Forbidden(APIError):
|
||||||
|
status_code = 403
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(APIError):
|
||||||
|
status_code = 404
|
||||||
|
|
||||||
|
|
||||||
|
class AlreadyExists(APIError):
|
||||||
|
status_code = 409
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointNotImplemented(APIError):
|
||||||
|
status_code = 501
|
||||||
7
src/cpl-api/cpl/api/logger.py
Normal file
7
src/cpl-api/cpl/api/logger.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from cpl.core.log.wrapped_logger import WrappedLogger
|
||||||
|
|
||||||
|
|
||||||
|
class APILogger(WrappedLogger):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
WrappedLogger.__init__(self, "api")
|
||||||
4
src/cpl-api/cpl/api/middleware/__init__.py
Normal file
4
src/cpl-api/cpl/api/middleware/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .authentication import AuthenticationMiddleware
|
||||||
|
from .authorization import AuthorizationMiddleware
|
||||||
|
from .logging import LoggingMiddleware
|
||||||
|
from .request import RequestMiddleware
|
||||||
80
src/cpl-api/cpl/api/middleware/authentication.py
Normal file
80
src/cpl-api/cpl/api/middleware/authentication.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
from keycloak import KeycloakAuthenticationError
|
||||||
|
from starlette.types import Scope, Receive, Send
|
||||||
|
|
||||||
|
from cpl.api.abc.asgi_middleware_abc import ASGIMiddleware
|
||||||
|
from cpl.api.error import Unauthorized
|
||||||
|
from cpl.api.logger import APILogger
|
||||||
|
from cpl.api.middleware.request import get_request
|
||||||
|
from cpl.api.router import Router
|
||||||
|
from cpl.auth.keycloak import KeycloakClient
|
||||||
|
from cpl.auth.schema import AuthUserDao, AuthUser
|
||||||
|
from cpl.core.ctx import set_user
|
||||||
|
from cpl.dependency import ServiceProviderABC
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticationMiddleware(ASGIMiddleware):
|
||||||
|
|
||||||
|
@ServiceProviderABC.inject
|
||||||
|
def __init__(self, app, logger: APILogger, keycloak: KeycloakClient, user_dao: AuthUserDao):
|
||||||
|
ASGIMiddleware.__init__(self, app)
|
||||||
|
|
||||||
|
self._logger = logger
|
||||||
|
|
||||||
|
self._keycloak = keycloak
|
||||||
|
self._user_dao = user_dao
|
||||||
|
|
||||||
|
async def __call__(self, scope: Scope, receive: Receive, send: Send):
|
||||||
|
request = get_request()
|
||||||
|
url = request.url.path
|
||||||
|
|
||||||
|
if url not in Router.get_auth_required_routes():
|
||||||
|
self._logger.trace(f"No authentication required for {url}")
|
||||||
|
return await self._app(scope, receive, send)
|
||||||
|
|
||||||
|
if not request.headers.get("Authorization"):
|
||||||
|
self._logger.debug(f"Unauthorized access to {url}, missing Authorization header")
|
||||||
|
return await Unauthorized(f"Missing header Authorization").asgi_response(scope, receive, send)
|
||||||
|
|
||||||
|
auth_header = request.headers.get("Authorization", None)
|
||||||
|
if not auth_header or not auth_header.startswith("Bearer "):
|
||||||
|
return await Unauthorized("Invalid Authorization header").asgi_response(scope, receive, send)
|
||||||
|
|
||||||
|
token = auth_header.split("Bearer ")[1]
|
||||||
|
if not await self._verify_login(token):
|
||||||
|
self._logger.debug(f"Unauthorized access to {url}, invalid token")
|
||||||
|
return await Unauthorized("Invalid token").asgi_response(scope, receive, send)
|
||||||
|
|
||||||
|
# check user exists in db, if not create
|
||||||
|
keycloak_id = self._keycloak.get_user_id(token)
|
||||||
|
if keycloak_id is None:
|
||||||
|
return await Unauthorized("Failed to get user id from token").asgi_response(scope, receive, send)
|
||||||
|
|
||||||
|
user = await self._get_or_crate_user(keycloak_id)
|
||||||
|
if user.deleted:
|
||||||
|
self._logger.debug(f"Unauthorized access to {url}, user is deleted")
|
||||||
|
return await Unauthorized("User is deleted").asgi_response(scope, receive, send)
|
||||||
|
|
||||||
|
request.state.user = user
|
||||||
|
set_user(user)
|
||||||
|
|
||||||
|
return await self._call_next(scope, receive, send)
|
||||||
|
|
||||||
|
async def _get_or_crate_user(self, keycloak_id: str) -> AuthUser:
|
||||||
|
existing = await self._user_dao.find_by_keycloak_id(keycloak_id)
|
||||||
|
if existing is not None:
|
||||||
|
return existing
|
||||||
|
|
||||||
|
user = AuthUser(0, keycloak_id)
|
||||||
|
uid = await self._user_dao.create(user)
|
||||||
|
return await self._user_dao.get_by_id(uid)
|
||||||
|
|
||||||
|
async def _verify_login(self, token: str) -> bool:
|
||||||
|
try:
|
||||||
|
token_info = self._keycloak.introspect(token)
|
||||||
|
return token_info.get("active", False)
|
||||||
|
except KeycloakAuthenticationError as e:
|
||||||
|
self._logger.debug(f"Keycloak authentication error: {e}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Unexpected error during token verification: {e}")
|
||||||
|
return False
|
||||||
73
src/cpl-api/cpl/api/middleware/authorization.py
Normal file
73
src/cpl-api/cpl/api/middleware/authorization.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
from starlette.types import Scope, Receive, Send
|
||||||
|
|
||||||
|
from cpl.api.abc.asgi_middleware_abc import ASGIMiddleware
|
||||||
|
from cpl.api.error import Unauthorized, Forbidden
|
||||||
|
from cpl.api.logger import APILogger
|
||||||
|
from cpl.api.middleware.request import get_request
|
||||||
|
from cpl.api.model.validation_match import ValidationMatch
|
||||||
|
from cpl.api.registry.policy import PolicyRegistry
|
||||||
|
from cpl.api.router import Router
|
||||||
|
from cpl.auth.schema._administration.auth_user_dao import AuthUserDao
|
||||||
|
from cpl.core.ctx.user_context import get_user
|
||||||
|
from cpl.dependency.service_provider_abc import ServiceProviderABC
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationMiddleware(ASGIMiddleware):
|
||||||
|
|
||||||
|
@ServiceProviderABC.inject
|
||||||
|
def __init__(self, app, logger: APILogger, policies: PolicyRegistry, user_dao: AuthUserDao):
|
||||||
|
ASGIMiddleware.__init__(self, app)
|
||||||
|
|
||||||
|
self._logger = logger
|
||||||
|
|
||||||
|
self._policies = policies
|
||||||
|
self._user_dao = user_dao
|
||||||
|
|
||||||
|
async def __call__(self, scope: Scope, receive: Receive, send: Send):
|
||||||
|
request = get_request()
|
||||||
|
url = request.url.path
|
||||||
|
|
||||||
|
if url not in Router.get_authorization_rules_paths():
|
||||||
|
self._logger.trace(f"No authorization required for {url}")
|
||||||
|
return await self._app(scope, receive, send)
|
||||||
|
|
||||||
|
user = get_user()
|
||||||
|
if not user:
|
||||||
|
return await Unauthorized(f"Unknown user").asgi_response(scope, receive, send)
|
||||||
|
|
||||||
|
roles = await user.roles
|
||||||
|
request.state.roles = roles
|
||||||
|
role_names = [r.name for r in roles]
|
||||||
|
|
||||||
|
perms = await user.permissions
|
||||||
|
request.state.permissions = perms
|
||||||
|
perm_names = [p.name for p in perms]
|
||||||
|
|
||||||
|
for rule in Router.get_authorization_rules():
|
||||||
|
match = rule["match"]
|
||||||
|
if rule["roles"]:
|
||||||
|
if match == ValidationMatch.all and not all(r in role_names for r in rule["roles"]):
|
||||||
|
return await Forbidden(f"missing roles: {rule["roles"]}").asgi_response(scope, receive, send)
|
||||||
|
if match == ValidationMatch.any and not any(r in role_names for r in rule["roles"]):
|
||||||
|
return await Forbidden(f"missing roles: {rule["roles"]}").asgi_response(scope, receive, send)
|
||||||
|
|
||||||
|
if rule["permissions"]:
|
||||||
|
if match == ValidationMatch.all and not all(p in perm_names for p in rule["permissions"]):
|
||||||
|
return await Forbidden(f"missing permissions: {rule["permissions"]}").asgi_response(
|
||||||
|
scope, receive, send
|
||||||
|
)
|
||||||
|
if match == ValidationMatch.any and not any(p in perm_names for p in rule["permissions"]):
|
||||||
|
return await Forbidden(f"missing permissions: {rule["permissions"]}").asgi_response(
|
||||||
|
scope, receive, send
|
||||||
|
)
|
||||||
|
|
||||||
|
for policy_name in rule["policies"]:
|
||||||
|
policy = self._policies.get(policy_name)
|
||||||
|
if not policy:
|
||||||
|
self._logger.warning(f"Authorization policy '{policy_name}' not found")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not await policy.resolve(user):
|
||||||
|
return await Forbidden(f"policy {policy.name} failed").asgi_response(scope, receive, send)
|
||||||
|
|
||||||
|
return await self._call_next(scope, receive, send)
|
||||||
87
src/cpl-api/cpl/api/middleware/logging.py
Normal file
87
src/cpl-api/cpl/api/middleware/logging.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.types import Receive, Scope, Send
|
||||||
|
|
||||||
|
from cpl.api.abc.asgi_middleware_abc import ASGIMiddleware
|
||||||
|
from cpl.api.logger import APILogger
|
||||||
|
from cpl.api.middleware.request import get_request
|
||||||
|
from cpl.dependency import ServiceProviderABC
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingMiddleware(ASGIMiddleware):
|
||||||
|
|
||||||
|
@ServiceProviderABC.inject
|
||||||
|
def __init__(self, app, logger: APILogger):
|
||||||
|
ASGIMiddleware.__init__(self, app)
|
||||||
|
|
||||||
|
self._logger = logger
|
||||||
|
|
||||||
|
async def __call__(self, scope: Scope, receive: Receive, send: Send):
|
||||||
|
if scope["type"] != "http":
|
||||||
|
await self._call_next(scope, receive, send)
|
||||||
|
return
|
||||||
|
|
||||||
|
request = get_request()
|
||||||
|
await self._log_request(request)
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
response_body = b""
|
||||||
|
status_code = 500
|
||||||
|
|
||||||
|
async def send_wrapper(message):
|
||||||
|
nonlocal response_body, status_code
|
||||||
|
if message["type"] == "http.response.start":
|
||||||
|
status_code = message["status"]
|
||||||
|
if message["type"] == "http.response.body":
|
||||||
|
response_body += message.get("body", b"")
|
||||||
|
await send(message)
|
||||||
|
|
||||||
|
await self._call_next(scope, receive, send_wrapper)
|
||||||
|
|
||||||
|
duration = (time.time() - start_time) * 1000
|
||||||
|
await self._log_after_request(request, status_code, duration)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _filter_relevant_headers(headers: dict) -> dict:
|
||||||
|
relevant_keys = {
|
||||||
|
"content-type",
|
||||||
|
"host",
|
||||||
|
"connection",
|
||||||
|
"user-agent",
|
||||||
|
"origin",
|
||||||
|
"referer",
|
||||||
|
"accept",
|
||||||
|
}
|
||||||
|
return {key: value for key, value in headers.items() if key in relevant_keys}
|
||||||
|
|
||||||
|
async def _log_request(self, request: Request):
|
||||||
|
self._logger.debug(
|
||||||
|
f"Request {getattr(request.state, 'request_id', '-')}: {request.method}@{request.url.path} from {request.client.host}"
|
||||||
|
)
|
||||||
|
|
||||||
|
from cpl.core.ctx.user_context import get_user
|
||||||
|
|
||||||
|
user = get_user()
|
||||||
|
|
||||||
|
request_info = {
|
||||||
|
"headers": self._filter_relevant_headers(dict(request.headers)),
|
||||||
|
"args": dict(request.query_params),
|
||||||
|
"form-data": (
|
||||||
|
await request.form()
|
||||||
|
if request.headers.get("content-type") == "application/x-www-form-urlencoded"
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"payload": (await request.json() if request.headers.get("content-length") == "0" else None),
|
||||||
|
"user": f"{user.id}-{user.keycloak_id}" if user else None,
|
||||||
|
"files": (
|
||||||
|
{key: file.filename for key, file in (await request.form()).items()} if await request.form() else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
self._logger.trace(f"Request {getattr(request.state, 'request_id', '-')}: {request_info}")
|
||||||
|
|
||||||
|
async def _log_after_request(self, request: Request, status_code: int, duration: float):
|
||||||
|
self._logger.info(
|
||||||
|
f"Request finished {getattr(request.state, 'request_id', '-')}: {status_code}-{request.method}@{request.url.path} from {request.client.host} in {duration:.2f}ms"
|
||||||
|
)
|
||||||
56
src/cpl-api/cpl/api/middleware/request.py
Normal file
56
src/cpl-api/cpl/api/middleware/request.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import time
|
||||||
|
from contextvars import ContextVar
|
||||||
|
from typing import Optional, Union
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.types import Scope, Receive, Send
|
||||||
|
|
||||||
|
from cpl.api.abc.asgi_middleware_abc import ASGIMiddleware
|
||||||
|
from cpl.api.logger import APILogger
|
||||||
|
from cpl.api.typing import TRequest
|
||||||
|
from cpl.dependency import ServiceProviderABC
|
||||||
|
|
||||||
|
_request_context: ContextVar[Union[TRequest, None]] = ContextVar("request", default=None)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestMiddleware(ASGIMiddleware):
|
||||||
|
|
||||||
|
@ServiceProviderABC.inject
|
||||||
|
def __init__(self, app, logger: APILogger):
|
||||||
|
ASGIMiddleware.__init__(self, app)
|
||||||
|
|
||||||
|
self._logger = logger
|
||||||
|
|
||||||
|
self._ctx_token = None
|
||||||
|
|
||||||
|
async def __call__(self, scope: Scope, receive: Receive, send: Send):
|
||||||
|
request = Request(scope, receive, send)
|
||||||
|
await self.set_request_data(request)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self._app(scope, receive, send)
|
||||||
|
finally:
|
||||||
|
await self.clean_request_data()
|
||||||
|
|
||||||
|
async def set_request_data(self, request: TRequest):
|
||||||
|
request.state.request_id = uuid4()
|
||||||
|
request.state.start_time = time.time()
|
||||||
|
self._logger.trace(f"Set new current request: {request.state.request_id}")
|
||||||
|
|
||||||
|
self._ctx_token = _request_context.set(request)
|
||||||
|
|
||||||
|
async def clean_request_data(self):
|
||||||
|
request = get_request()
|
||||||
|
if request is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._ctx_token is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._logger.trace(f"Clearing current request: {request.state.request_id}")
|
||||||
|
_request_context.reset(self._ctx_token)
|
||||||
|
|
||||||
|
|
||||||
|
def get_request() -> Optional[TRequest]:
|
||||||
|
return _request_context.get()
|
||||||
3
src/cpl-api/cpl/api/model/__init__.py
Normal file
3
src/cpl-api/cpl/api/model/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .api_route import ApiRoute
|
||||||
|
from .policy import Policy
|
||||||
|
from .validation_match import ValidationMatch
|
||||||
43
src/cpl-api/cpl/api/model/api_route.py
Normal file
43
src/cpl-api/cpl/api/model/api_route.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from starlette.routing import Route
|
||||||
|
|
||||||
|
from cpl.api.typing import HTTPMethods
|
||||||
|
|
||||||
|
|
||||||
|
class ApiRoute:
|
||||||
|
|
||||||
|
def __init__(self, path: str, fn: Callable, method: HTTPMethods, **kwargs):
|
||||||
|
self._path = path
|
||||||
|
self._fn = fn
|
||||||
|
self._method = method
|
||||||
|
|
||||||
|
self._kwargs = kwargs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self._fn.__name__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fn(self) -> Callable:
|
||||||
|
return self._fn
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self) -> str:
|
||||||
|
return self._path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def method(self) -> HTTPMethods:
|
||||||
|
return self._method
|
||||||
|
|
||||||
|
@property
|
||||||
|
def kwargs(self) -> dict:
|
||||||
|
return self._kwargs
|
||||||
|
|
||||||
|
def to_starlette(self, wrap_endpoint: Callable = None) -> Route:
|
||||||
|
return Route(
|
||||||
|
self._path,
|
||||||
|
self._fn if not wrap_endpoint else wrap_endpoint(self._fn),
|
||||||
|
methods=[self._method],
|
||||||
|
**self._kwargs,
|
||||||
|
)
|
||||||
34
src/cpl-api/cpl/api/model/policy.py
Normal file
34
src/cpl-api/cpl/api/model/policy.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from asyncio import iscoroutinefunction
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from cpl.api.typing import PolicyResolver
|
||||||
|
from cpl.core.ctx import get_user
|
||||||
|
|
||||||
|
|
||||||
|
class Policy:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
resolver: PolicyResolver = None,
|
||||||
|
):
|
||||||
|
self._name = name
|
||||||
|
self._resolver: Optional[PolicyResolver] = resolver
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def resolvers(self) -> PolicyResolver:
|
||||||
|
return self._resolver
|
||||||
|
|
||||||
|
async def resolve(self, *args, **kwargs) -> bool:
|
||||||
|
if not self._resolver:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if callable(self._resolver):
|
||||||
|
if iscoroutinefunction(self._resolver):
|
||||||
|
return await self._resolver(get_user())
|
||||||
|
|
||||||
|
return self._resolver(get_user())
|
||||||
|
return False
|
||||||
6
src/cpl-api/cpl/api/model/validation_match.py
Normal file
6
src/cpl-api/cpl/api/model/validation_match.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationMatch(Enum):
|
||||||
|
any = "any"
|
||||||
|
all = "all"
|
||||||
2
src/cpl-api/cpl/api/registry/__init__.py
Normal file
2
src/cpl-api/cpl/api/registry/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .policy import PolicyRegistry
|
||||||
|
from .route import RouteRegistry
|
||||||
28
src/cpl-api/cpl/api/registry/policy.py
Normal file
28
src/cpl-api/cpl/api/registry/policy.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from cpl.api.model.policy import Policy
|
||||||
|
from cpl.core.abc.registry_abc import RegistryABC
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyRegistry(RegistryABC):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
RegistryABC.__init__(self)
|
||||||
|
|
||||||
|
def extend(self, items: list[Policy]):
|
||||||
|
for policy in items:
|
||||||
|
self.add(policy)
|
||||||
|
|
||||||
|
def add(self, item: Policy):
|
||||||
|
assert isinstance(item, Policy), "policy must be an instance of Policy"
|
||||||
|
|
||||||
|
if item.name in self._items:
|
||||||
|
raise ValueError(f"Policy {item.name} is already registered")
|
||||||
|
|
||||||
|
self._items[item.name] = item
|
||||||
|
|
||||||
|
def get(self, key: str) -> Optional[Policy]:
|
||||||
|
return self._items.get(key)
|
||||||
|
|
||||||
|
def all(self) -> list[Policy]:
|
||||||
|
return list(self._items.values())
|
||||||
32
src/cpl-api/cpl/api/registry/route.py
Normal file
32
src/cpl-api/cpl/api/registry/route.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from cpl.api.model.api_route import ApiRoute
|
||||||
|
from cpl.core.abc.registry_abc import RegistryABC
|
||||||
|
|
||||||
|
|
||||||
|
class RouteRegistry(RegistryABC):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
RegistryABC.__init__(self)
|
||||||
|
|
||||||
|
def extend(self, items: list[ApiRoute]):
|
||||||
|
for policy in items:
|
||||||
|
self.add(policy)
|
||||||
|
|
||||||
|
def add(self, item: ApiRoute):
|
||||||
|
assert isinstance(item, ApiRoute), "route must be an instance of ApiRoute"
|
||||||
|
|
||||||
|
if item.path in self._items:
|
||||||
|
raise ValueError(f"ApiRoute {item.path} is already registered")
|
||||||
|
|
||||||
|
self._items[item.path] = item
|
||||||
|
|
||||||
|
def set(self, item: ApiRoute):
|
||||||
|
assert isinstance(item, ApiRoute), "route must be an instance of ApiRoute"
|
||||||
|
self._items[item.path] = item
|
||||||
|
|
||||||
|
def get(self, key: str) -> Optional[ApiRoute]:
|
||||||
|
return self._items.get(key)
|
||||||
|
|
||||||
|
def all(self) -> list[ApiRoute]:
|
||||||
|
return list(self._items.values())
|
||||||
164
src/cpl-api/cpl/api/router.py
Normal file
164
src/cpl-api/cpl/api/router.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from cpl.api.model.validation_match import ValidationMatch
|
||||||
|
from cpl.api.registry.route import RouteRegistry
|
||||||
|
from cpl.api.typing import HTTPMethods
|
||||||
|
|
||||||
|
|
||||||
|
class Router:
|
||||||
|
_auth_required: list[str] = []
|
||||||
|
_authorization_rules: dict[str, dict] = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_auth_required_routes(cls) -> list[str]:
|
||||||
|
return cls._auth_required
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_authorization_rules_paths(cls) -> list[str]:
|
||||||
|
return list(cls._authorization_rules.keys())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_authorization_rules(cls) -> list[dict]:
|
||||||
|
return list(cls._authorization_rules.values())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def authenticate(cls):
|
||||||
|
"""
|
||||||
|
Decorator to mark a route as requiring authentication.
|
||||||
|
Usage:
|
||||||
|
@Route.authenticate()
|
||||||
|
@Route.get("/example")
|
||||||
|
async def example_endpoint(request: TRequest):
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
|
||||||
|
def inner(fn):
|
||||||
|
route_path = getattr(fn, "_route_path", None)
|
||||||
|
if route_path and route_path not in cls._auth_required:
|
||||||
|
cls._auth_required.append(route_path)
|
||||||
|
return fn
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def authorize(
|
||||||
|
cls,
|
||||||
|
roles: list[str | Enum] = None,
|
||||||
|
permissions: list[str | Enum] = None,
|
||||||
|
policies: list[str] = None,
|
||||||
|
match: ValidationMatch = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Decorator to mark a route as requiring authorization.
|
||||||
|
Usage:
|
||||||
|
@Route.authorize()
|
||||||
|
@Route.get("/example")
|
||||||
|
async def example_endpoint(request: TRequest):
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
assert roles is None or isinstance(roles, list), "roles must be a list of strings"
|
||||||
|
assert permissions is None or isinstance(permissions, list), "permissions must be a list of strings"
|
||||||
|
assert policies is None or isinstance(policies, list), "policies must be a list of strings"
|
||||||
|
assert match is None or isinstance(match, ValidationMatch), "match must be an instance of ValidationMatch"
|
||||||
|
|
||||||
|
if roles is not None:
|
||||||
|
for role in roles:
|
||||||
|
if isinstance(role, Enum):
|
||||||
|
roles[roles.index(role)] = role.value
|
||||||
|
|
||||||
|
if permissions is not None:
|
||||||
|
for perm in permissions:
|
||||||
|
if isinstance(perm, Enum):
|
||||||
|
permissions[permissions.index(perm)] = perm.value
|
||||||
|
|
||||||
|
def inner(fn):
|
||||||
|
path = getattr(fn, "_route_path", None)
|
||||||
|
if not path:
|
||||||
|
return fn
|
||||||
|
|
||||||
|
if path in cls._authorization_rules:
|
||||||
|
raise ValueError(f"Route {path} is already registered for authorization")
|
||||||
|
|
||||||
|
cls._authorization_rules[path] = {
|
||||||
|
"roles": roles or [],
|
||||||
|
"permissions": permissions or [],
|
||||||
|
"policies": policies or [],
|
||||||
|
"match": match or ValidationMatch.all,
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def route(cls, path: str, method: HTTPMethods, registry: RouteRegistry = None, **kwargs):
|
||||||
|
from cpl.api.model.api_route import ApiRoute
|
||||||
|
|
||||||
|
if not registry:
|
||||||
|
from cpl.dependency.service_provider_abc import ServiceProviderABC
|
||||||
|
|
||||||
|
routes = ServiceProviderABC.get_global_service(RouteRegistry)
|
||||||
|
else:
|
||||||
|
routes = registry
|
||||||
|
|
||||||
|
def inner(fn):
|
||||||
|
routes.add(ApiRoute(path, fn, method, **kwargs))
|
||||||
|
setattr(fn, "_route_path", path)
|
||||||
|
return fn
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, path: str, **kwargs):
|
||||||
|
return cls.route(path, "GET", **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def head(cls, path: str, **kwargs):
|
||||||
|
return cls.route(path, "HEAD", **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def post(cls, path: str, **kwargs):
|
||||||
|
return cls.route(path, "POST", **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def put(cls, path: str, **kwargs):
|
||||||
|
return cls.route(path, "PUT", **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def patch(cls, path: str, **kwargs):
|
||||||
|
return cls.route(path, "PATCH", **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def delete(cls, path: str, **kwargs):
|
||||||
|
return cls.route(path, "DELETE", **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def override(cls):
|
||||||
|
"""
|
||||||
|
Decorator to override an existing route with the same path.
|
||||||
|
Usage:
|
||||||
|
@Route.override()
|
||||||
|
@Route.get("/example")
|
||||||
|
async def example_endpoint(request: TRequest):
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
|
||||||
|
from cpl.api.model.api_route import ApiRoute
|
||||||
|
from cpl.dependency.service_provider_abc import ServiceProviderABC
|
||||||
|
|
||||||
|
routes = ServiceProviderABC.get_global_service(RouteRegistry)
|
||||||
|
|
||||||
|
def inner(fn):
|
||||||
|
path = getattr(fn, "_route_path", None)
|
||||||
|
if path is None:
|
||||||
|
raise ValueError("Cannot override a route that has not been registered yet")
|
||||||
|
|
||||||
|
route = routes.get(path)
|
||||||
|
if route is None:
|
||||||
|
raise ValueError(f"Cannot override a route that does not exist: {path}")
|
||||||
|
|
||||||
|
routes.add(ApiRoute(path, fn, route.method, **route.kwargs))
|
||||||
|
setattr(fn, "_route_path", path)
|
||||||
|
return fn
|
||||||
|
|
||||||
|
return inner
|
||||||
13
src/cpl-api/cpl/api/settings.py
Normal file
13
src/cpl-api/cpl/api/settings.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from cpl.core.configuration import ConfigurationModelABC
|
||||||
|
|
||||||
|
|
||||||
|
class ApiSettings(ConfigurationModelABC):
|
||||||
|
|
||||||
|
def __init__(self, src: Optional[dict] = None):
|
||||||
|
super().__init__(src)
|
||||||
|
|
||||||
|
self.option("host", str, "0.0.0.0")
|
||||||
|
self.option("port", int, 5000)
|
||||||
|
self.option("allowed_origins", list[str])
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user