mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-23 19:44:20 +00:00
__magic__
This commit is contained in:
parent
a33144f2f9
commit
b44fcc4bf4
16 changed files with 110 additions and 84 deletions
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "royalnet"
|
name = "royalnet"
|
||||||
version = "5.9.3"
|
version = "5.10.0"
|
||||||
description = "A multipurpose bot and web framework"
|
description = "A multipurpose bot and web framework"
|
||||||
authors = ["Stefano Pigozzi <ste.pigozzi@gmail.com>"]
|
authors = ["Stefano Pigozzi <ste.pigozzi@gmail.com>"]
|
||||||
license = "AGPL-3.0+"
|
license = "AGPL-3.0+"
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
import datetime
|
import datetime
|
||||||
import royalnet.utils as ru
|
import royalnet.utils as ru
|
||||||
from royalnet.constellation.api import *
|
import royalnet.constellation.api as rca
|
||||||
from royalnet.constellation.api.apierrors import *
|
import royalnet.constellation.api.apierrors as rcae
|
||||||
from ..tables.users import User
|
from ..tables.users import User
|
||||||
from ..tables.tokens import Token
|
from ..tables.tokens import Token
|
||||||
|
|
||||||
|
|
||||||
class ApiAuthLoginRoyalnetStar(ApiStar):
|
class ApiAuthLoginRoyalnetStar(rca.ApiStar):
|
||||||
path = "/api/auth/login/royalnet/v1"
|
path = "/api/auth/login/royalnet/v1"
|
||||||
|
|
||||||
methods = ["POST"]
|
|
||||||
|
|
||||||
parameters = {
|
parameters = {
|
||||||
"post": {
|
"post": {
|
||||||
"username": "The name of the user you are logging in as.",
|
"username": "The name of the user you are logging in as.",
|
||||||
|
@ -20,7 +18,8 @@ class ApiAuthLoginRoyalnetStar(ApiStar):
|
||||||
|
|
||||||
tags = ["auth"]
|
tags = ["auth"]
|
||||||
|
|
||||||
async def post(self, data: ApiData) -> ru.JSON:
|
@rca.magic
|
||||||
|
async def post(self, data: rca.ApiData) -> ru.JSON:
|
||||||
"""Login with a Royalnet account.
|
"""Login with a Royalnet account.
|
||||||
|
|
||||||
The method returns a API token valid for 7 days that identifies and authenticates the user to the API.
|
The method returns a API token valid for 7 days that identifies and authenticates the user to the API.
|
||||||
|
@ -36,10 +35,10 @@ class ApiAuthLoginRoyalnetStar(ApiStar):
|
||||||
async with self.session_acm() as session:
|
async with self.session_acm() as session:
|
||||||
user: User = await ru.asyncify(session.query(UserT).filter_by(username=username).one_or_none)
|
user: User = await ru.asyncify(session.query(UserT).filter_by(username=username).one_or_none)
|
||||||
if user is None:
|
if user is None:
|
||||||
raise NotFoundError("User not found")
|
raise rcae.NotFoundError("User not found")
|
||||||
pswd_check = user.test_password(password)
|
pswd_check = user.test_password(password)
|
||||||
if not pswd_check:
|
if not pswd_check:
|
||||||
raise UnauthorizedError("Invalid password")
|
raise rcae.UnauthorizedError("Invalid password")
|
||||||
token: Token = TokenT.generate(alchemy=self.alchemy, user=user, expiration_delta=datetime.timedelta(days=7))
|
token: Token = TokenT.generate(alchemy=self.alchemy, user=user, expiration_delta=datetime.timedelta(days=7))
|
||||||
session.add(token)
|
session.add(token)
|
||||||
await ru.asyncify(session.commit)
|
await ru.asyncify(session.commit)
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
from typing import *
|
from typing import *
|
||||||
import datetime
|
import datetime
|
||||||
import royalnet.utils as ru
|
import royalnet.utils as ru
|
||||||
from royalnet.constellation.api import *
|
import royalnet.constellation.api as rca
|
||||||
from ..tables.tokens import Token
|
from ..tables.tokens import Token
|
||||||
|
|
||||||
|
|
||||||
class ApiAuthTokenStar(ApiStar):
|
class ApiAuthTokenStar(rca.ApiStar):
|
||||||
path = "/api/auth/token/v1"
|
path = "/api/auth/token/v1"
|
||||||
|
|
||||||
methods = ["GET", "POST"]
|
|
||||||
|
|
||||||
parameters = {
|
parameters = {
|
||||||
"get": {},
|
"get": {},
|
||||||
"post": {
|
"post": {
|
||||||
|
@ -24,12 +22,14 @@ class ApiAuthTokenStar(ApiStar):
|
||||||
|
|
||||||
tags = ["auth"]
|
tags = ["auth"]
|
||||||
|
|
||||||
async def get(self, data: ApiData) -> ru.JSON:
|
@rca.magic
|
||||||
|
async def get(self, data: rca.ApiData) -> ru.JSON:
|
||||||
"""Get information about the current login token."""
|
"""Get information about the current login token."""
|
||||||
token = await data.token()
|
token = await data.token()
|
||||||
return token.json()
|
return token.json()
|
||||||
|
|
||||||
async def post(self, data: ApiData) -> ru.JSON:
|
@rca.magic
|
||||||
|
async def post(self, data: rca.ApiData) -> ru.JSON:
|
||||||
"""Create a new login token for the authenticated user.
|
"""Create a new login token for the authenticated user.
|
||||||
|
|
||||||
Keep it secret, as it is basically a password!"""
|
Keep it secret, as it is basically a password!"""
|
||||||
|
@ -37,7 +37,7 @@ class ApiAuthTokenStar(ApiStar):
|
||||||
try:
|
try:
|
||||||
duration = int(data["duration"])
|
duration = int(data["duration"])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise InvalidParameterError("Duration is not a valid integer")
|
raise rca.InvalidParameterError("Duration is not a valid integer")
|
||||||
new_token = Token.generate(self.alchemy, user, datetime.timedelta(seconds=duration))
|
new_token = Token.generate(self.alchemy, user, datetime.timedelta(seconds=duration))
|
||||||
data.session.add(new_token)
|
data.session.add(new_token)
|
||||||
await data.session_commit()
|
await data.session_commit()
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import royalnet.version as rv
|
import royalnet.version as rv
|
||||||
from royalnet.constellation.api import *
|
import royalnet.constellation.api as rca
|
||||||
import royalnet.utils as ru
|
import royalnet.utils as ru
|
||||||
|
|
||||||
|
|
||||||
class ApiRoyalnetVersionStar(ApiStar):
|
class ApiRoyalnetVersionStar(rca.ApiStar):
|
||||||
path = "/api/royalnet/version/v1"
|
path = "/api/royalnet/version/v1"
|
||||||
|
|
||||||
methods = ["GET"]
|
|
||||||
|
|
||||||
tags = ["royalnet"]
|
tags = ["royalnet"]
|
||||||
|
|
||||||
async def get(self, data: ApiData) -> ru.JSON:
|
@rca.magic
|
||||||
|
async def get(self, data: rca.ApiData) -> ru.JSON:
|
||||||
"""Get the current Royalnet version."""
|
"""Get the current Royalnet version."""
|
||||||
return {
|
return {
|
||||||
"semantic": rv.semantic
|
"semantic": rv.semantic
|
||||||
|
|
|
@ -9,8 +9,6 @@ from ..tables import *
|
||||||
class ApiUserCreateStar(rca.ApiStar):
|
class ApiUserCreateStar(rca.ApiStar):
|
||||||
path = "/api/user/create/v1"
|
path = "/api/user/create/v1"
|
||||||
|
|
||||||
methods = ["POST"]
|
|
||||||
|
|
||||||
parameters = {
|
parameters = {
|
||||||
"post": {
|
"post": {
|
||||||
"username": "The name of the user you are creating.",
|
"username": "The name of the user you are creating.",
|
||||||
|
|
|
@ -1,22 +1,20 @@
|
||||||
from royalnet.utils import *
|
import royalnet.utils as ru
|
||||||
from royalnet.backpack.tables import *
|
import royalnet.backpack.tables as rbt
|
||||||
from royalnet.constellation.api import *
|
import royalnet.constellation.api as rca
|
||||||
|
|
||||||
|
|
||||||
class ApiUserFindStar(ApiStar):
|
class ApiUserFindStar(rca.ApiStar):
|
||||||
path = "/api/user/find/v1"
|
path = "/api/user/find/v1"
|
||||||
|
|
||||||
tags = ["user"]
|
|
||||||
|
|
||||||
parameters = {
|
parameters = {
|
||||||
"get": {
|
"get": {
|
||||||
"alias": "One of the aliases of the user to get."
|
"alias": "One of the aliases of the user to get."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async def get(self, data: ApiData) -> dict:
|
async def get(self, data: rca.ApiData) -> ru.JSON:
|
||||||
"""Get details about the Royalnet user with a certain alias."""
|
"""Get details about the Royalnet user with a certain alias."""
|
||||||
user = await User.find(self.alchemy, data.session, data["alias"])
|
user = await rbt.User.find(self.alchemy, data.session, data["alias"])
|
||||||
if user is None:
|
if user is None:
|
||||||
raise NotFoundError("No such user.")
|
raise rca.NotFoundError("No such user.")
|
||||||
return user.json()
|
return user.json()
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
from royalnet.utils import *
|
import royalnet.utils as ru
|
||||||
from royalnet.backpack.tables import *
|
import royalnet.backpack.tables as rbt
|
||||||
from royalnet.constellation.api import *
|
import royalnet.constellation.api as rca
|
||||||
|
|
||||||
|
|
||||||
class ApiUserGetStar(ApiStar):
|
class ApiUserGetStar(rca.ApiStar):
|
||||||
path = "/api/user/get/v1"
|
path = "/api/user/get/v1"
|
||||||
|
|
||||||
parameters = {
|
parameters = {
|
||||||
|
@ -14,14 +14,15 @@ class ApiUserGetStar(ApiStar):
|
||||||
|
|
||||||
tags = ["user"]
|
tags = ["user"]
|
||||||
|
|
||||||
async def get(self, data: ApiData) -> dict:
|
@rca.magic
|
||||||
|
async def get(self, data: rca.ApiData) -> dict:
|
||||||
"""Get details about the Royalnet user with a certain id."""
|
"""Get details about the Royalnet user with a certain id."""
|
||||||
user_id_str = data["id"]
|
user_id_str = data["id"]
|
||||||
try:
|
try:
|
||||||
user_id = int(user_id_str)
|
user_id = int(user_id_str)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
raise InvalidParameterError("'id' is not a valid int.")
|
raise rca.InvalidParameterError("'id' is not a valid int.")
|
||||||
user: User = await asyncify(data.session.query(self.alchemy.get(User)).get, user_id)
|
user: rbt.User = await ru.asyncify(data.session.query(self.alchemy.get(rbt.User)).get, user_id)
|
||||||
if user is None:
|
if user is None:
|
||||||
raise NotFoundError("No such user.")
|
raise rca.NotFoundError("No such user.")
|
||||||
return user.json()
|
return user.json()
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
from starlette.responses import *
|
from starlette.responses import *
|
||||||
from royalnet.utils import *
|
import royalnet.utils as ru
|
||||||
from royalnet.backpack.tables import *
|
import royalnet.backpack.tables as rbt
|
||||||
from royalnet.constellation.api import *
|
import royalnet.constellation.api as rca
|
||||||
|
|
||||||
|
|
||||||
class ApiUserListStar(ApiStar):
|
class ApiUserListStar(rca.ApiStar):
|
||||||
path = "/api/user/list/v1"
|
path = "/api/user/list/v1"
|
||||||
|
|
||||||
tags = ["user"]
|
tags = ["user"]
|
||||||
|
|
||||||
async def get(self, data: ApiData) -> JSON:
|
@rca.magic
|
||||||
"Get a list of all Royalnet users."
|
async def get(self, data: rca.ApiData) -> ru.JSON:
|
||||||
users: typing.List[User] = await asyncify(data.session.query(self.alchemy.get(User)).all)
|
"""Get a list of all Royalnet users."""
|
||||||
|
users: typing.List[rbt.User] = await ru.asyncify(data.session.query(self.alchemy.get(rbt.User)).all)
|
||||||
return [user.json() for user in users]
|
return [user.json() for user in users]
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
from typing import *
|
from typing import *
|
||||||
import datetime
|
import datetime
|
||||||
import royalnet.utils as ru
|
import royalnet.utils as ru
|
||||||
from royalnet.constellation.api import *
|
import royalnet.constellation.api as rca
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
from ..tables.tokens import Token
|
from ..tables.tokens import Token
|
||||||
|
|
||||||
|
|
||||||
class ApiUserPasswd(ApiStar):
|
class ApiUserPasswd(rca.ApiStar):
|
||||||
path = "/api/user/passwd/v1"
|
path = "/api/user/passwd/v1"
|
||||||
|
|
||||||
methods = ["PUT"]
|
|
||||||
|
|
||||||
tags = ["user"]
|
tags = ["user"]
|
||||||
|
|
||||||
parameters = {
|
parameters = {
|
||||||
|
@ -25,7 +23,8 @@ class ApiUserPasswd(ApiStar):
|
||||||
|
|
||||||
requires_auth = True
|
requires_auth = True
|
||||||
|
|
||||||
async def put(self, data: ApiData) -> ru.JSON:
|
@rca.magic
|
||||||
|
async def put(self, data: rca.ApiData) -> ru.JSON:
|
||||||
"""Change the password of the currently logged in user.
|
"""Change the password of the currently logged in user.
|
||||||
|
|
||||||
This method also revokes all the issued tokens for the user."""
|
This method also revokes all the issued tokens for the user."""
|
||||||
|
|
|
@ -11,6 +11,7 @@ from .apierrors import \
|
||||||
InvalidParameterError, \
|
InvalidParameterError, \
|
||||||
MethodNotImplementedError, \
|
MethodNotImplementedError, \
|
||||||
UnsupportedError
|
UnsupportedError
|
||||||
|
from .magic import magic
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -31,4 +32,5 @@ __all__ = [
|
||||||
"InvalidParameterError",
|
"InvalidParameterError",
|
||||||
"MethodNotImplementedError",
|
"MethodNotImplementedError",
|
||||||
"UnsupportedError",
|
"UnsupportedError",
|
||||||
|
"magic",
|
||||||
]
|
]
|
||||||
|
|
|
@ -20,6 +20,7 @@ class ApiStar(PageStar, ABC):
|
||||||
deprecated: Dict[str, bool] = {}
|
deprecated: Dict[str, bool] = {}
|
||||||
|
|
||||||
tags: List[str] = []
|
tags: List[str] = []
|
||||||
|
__override__: List[str] = []
|
||||||
|
|
||||||
async def page(self, request: Request) -> JSONResponse:
|
async def page(self, request: Request) -> JSONResponse:
|
||||||
if request.query_params:
|
if request.query_params:
|
||||||
|
@ -42,23 +43,25 @@ class ApiStar(PageStar, ABC):
|
||||||
response = await self.put(apidata)
|
response = await self.put(apidata)
|
||||||
elif method == "delete":
|
elif method == "delete":
|
||||||
response = await self.delete(apidata)
|
response = await self.delete(apidata)
|
||||||
|
elif method == "options":
|
||||||
|
return api_success("Preflight allowed.", methods=self.methods())
|
||||||
else:
|
else:
|
||||||
raise MethodNotImplementedError("Unknown method")
|
raise MethodNotImplementedError("Unknown method")
|
||||||
except UnauthorizedError as e:
|
except UnauthorizedError as e:
|
||||||
return api_error(e, code=401)
|
return api_error(e, code=401, methods=self.methods())
|
||||||
except NotFoundError as e:
|
except NotFoundError as e:
|
||||||
return api_error(e, code=404)
|
return api_error(e, code=404, methods=self.methods())
|
||||||
except ForbiddenError as e:
|
except ForbiddenError as e:
|
||||||
return api_error(e, code=403)
|
return api_error(e, code=403, methods=self.methods())
|
||||||
except MethodNotImplementedError as e:
|
except MethodNotImplementedError as e:
|
||||||
return api_error(e, code=405)
|
return api_error(e, code=405, methods=self.methods())
|
||||||
except BadRequestError as e:
|
except BadRequestError as e:
|
||||||
return api_error(e, code=400)
|
return api_error(e, code=400, methods=self.methods())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
ru.sentry_exc(e)
|
ru.sentry_exc(e)
|
||||||
return api_error(e, code=500)
|
return api_error(e, code=500, methods=self.methods())
|
||||||
else:
|
else:
|
||||||
return api_success(response)
|
return api_success(response, methods=self.methods())
|
||||||
finally:
|
finally:
|
||||||
await apidata.session_close()
|
await apidata.session_close()
|
||||||
|
|
||||||
|
@ -107,10 +110,23 @@ class ApiStar(PageStar, ABC):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def methods(cls):
|
||||||
|
magics = []
|
||||||
|
for key, value in cls.__dict__.items():
|
||||||
|
attr = value
|
||||||
|
try:
|
||||||
|
magic = attr.__magic__
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
if magic:
|
||||||
|
magics.append(key)
|
||||||
|
return [*magics, "options"]
|
||||||
|
|
||||||
def swagger(self) -> ru.JSON:
|
def swagger(self) -> ru.JSON:
|
||||||
"""Generate one or more swagger paths for this ApiStar."""
|
"""Generate one or more swagger paths for this ApiStar."""
|
||||||
result = {}
|
result = {}
|
||||||
for method in self.methods:
|
for method in self.methods():
|
||||||
result[method.lower()] = self.__swagger_for_a_method(self.__getattribute__(method.lower()))
|
result[method.lower()] = self.__swagger_for_a_method(self.__getattribute__(method.lower()))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,38 @@
|
||||||
from typing import *
|
from typing import *
|
||||||
|
import royalnet.utils as ru
|
||||||
try:
|
try:
|
||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
except ImportError:
|
except ImportError:
|
||||||
JSONResponse = None
|
JSONResponse = None
|
||||||
|
|
||||||
|
|
||||||
def api_response(data: dict, code: int, headers: dict = None) -> JSONResponse:
|
def api_response(data: ru.JSON, code: int, headers: dict = None, methods=None) -> JSONResponse:
|
||||||
if headers is None:
|
if headers is None:
|
||||||
headers = {}
|
headers = {}
|
||||||
|
if methods is None:
|
||||||
|
methods = ["GET"]
|
||||||
|
|
||||||
full_headers = {
|
full_headers = {
|
||||||
**headers,
|
**headers,
|
||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": ", ".join(methods).upper()
|
||||||
}
|
}
|
||||||
return JSONResponse(data, status_code=code, headers=full_headers)
|
return JSONResponse(data, status_code=code, headers=full_headers)
|
||||||
|
|
||||||
|
|
||||||
def api_success(data: dict) -> JSONResponse:
|
def api_success(data: ru.JSON, methods=None) -> JSONResponse:
|
||||||
result = {
|
result = {
|
||||||
"success": True,
|
"success": True,
|
||||||
"data": data
|
"data": data
|
||||||
}
|
}
|
||||||
return api_response(result, code=200)
|
return api_response(result, code=200, methods=methods)
|
||||||
|
|
||||||
|
|
||||||
def api_error(error: Exception, code: int = 500) -> JSONResponse:
|
def api_error(error: Exception, code: int = 500, methods=None) -> JSONResponse:
|
||||||
result = {
|
result = {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error_type": error.__class__.__qualname__,
|
"error_type": error.__class__.__qualname__,
|
||||||
"error_args": list(error.args),
|
"error_args": list(error.args),
|
||||||
"error_code": code,
|
"error_code": code,
|
||||||
}
|
}
|
||||||
return api_response(result, code=code)
|
return api_response(result, code=code, methods=methods)
|
||||||
|
|
15
royalnet/constellation/api/magic.py
Normal file
15
royalnet/constellation/api/magic.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
from typing import *
|
||||||
|
import functools
|
||||||
|
|
||||||
|
|
||||||
|
def magic(func):
|
||||||
|
# i made this at 2:00 am
|
||||||
|
# at 2:15 am i already had no idea on how it worked
|
||||||
|
# at 2:30 i still have no idea but it works appearently
|
||||||
|
# 2:40 TODO: document me
|
||||||
|
func.__magic__ = True
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
async def f(*args, **kwargs):
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
return f
|
|
@ -98,16 +98,7 @@ class Constellation:
|
||||||
self.events: Dict[str, rc.Event] = {}
|
self.events: Dict[str, rc.Event] = {}
|
||||||
"""A dictionary containing all :class:`~rc.Event` that can be handled by this :class:`Constellation`."""
|
"""A dictionary containing all :class:`~rc.Event` that can be handled by this :class:`Constellation`."""
|
||||||
|
|
||||||
middleware = []
|
self.starlette = starlette.applications.Starlette(debug=__debug__)
|
||||||
if constellation_cfg.get("cors_middleware", False):
|
|
||||||
log.info("CORS middleware: enabled")
|
|
||||||
middleware.append(
|
|
||||||
starlette.middleware.Middleware(starlette.middleware.cors.CORSMiddleware, {"allow_origins": ["*"]})
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
log.info("CORS middleware: disabled")
|
|
||||||
|
|
||||||
self.starlette = starlette.applications.Starlette(debug=__debug__, middleware=middleware)
|
|
||||||
"""The :class:`~starlette.Starlette` app."""
|
"""The :class:`~starlette.Starlette` app."""
|
||||||
|
|
||||||
self.stars: List[PageStar] = []
|
self.stars: List[PageStar] = []
|
||||||
|
@ -269,7 +260,7 @@ class Constellation:
|
||||||
log.info(f"Running {page_star}")
|
log.info(f"Running {page_star}")
|
||||||
return await page_star.page(request)
|
return await page_star.page(request)
|
||||||
|
|
||||||
return page_star.path, f, page_star.methods
|
return page_star.path, f, page_star.methods()
|
||||||
|
|
||||||
def register_page_stars(self, page_stars: List[Type[PageStar]], pack_cfg: Dict[str, Any]):
|
def register_page_stars(self, page_stars: List[Type[PageStar]], pack_cfg: Dict[str, Any]):
|
||||||
for SelectedPageStar in page_stars:
|
for SelectedPageStar in page_stars:
|
||||||
|
|
|
@ -18,17 +18,19 @@ class PageStar(Star):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
methods: List[str] = ["GET"]
|
@classmethod
|
||||||
"""The HTTP methods supported by the Star, in form of a list.
|
def methods(cls):
|
||||||
|
"""The HTTP methods supported by the Star, in form of a list.
|
||||||
|
|
||||||
By default, a Star only supports the ``GET`` method, but more can be added.
|
By default, a Star only supports the ``GET`` method, but more can be added.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
::
|
::
|
||||||
|
|
||||||
methods: List[str] = ["GET", "POST", "PUT", "DELETE"]
|
methods: List[str] = ["GET", "POST", "PUT", "DELETE"]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
return ["GET"]
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__qualname__}: {self.path}>"
|
return f"<{self.__class__.__qualname__}: {self.path}>"
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
semantic = "5.9.3"
|
semantic = "5.10.0"
|
||||||
|
|
Loading…
Reference in a new issue