код происхождения:
# add this class to make the code can be a reproducible example.In acutually environment all attribute could be True or False.
class Capabilities:
def __init__(self) -> None:
self.canEditCompany = True
self.canDeleteCompany = True
self.canShareCompany = True
self.canAddFiscal = True
self.canAddCategory = True
self.canEditCategory = True
self.canGetCategory = True
self.canAddAccount = True
self.canGetAccount = True
self.canEditAccount = True
self.canAddTransaction = True
self.canAddScan = True
self.canAddTax = True
def permission_control(user_email,company):
capabilities = Capabilities()
casbin_roles = []
if capabilities.canEditCompany:
casbin_roles.append((user_email, company, 'PUT'))
if capabilities.canDeleteCompany:
casbin_roles.append((user_email, company, 'Delete'))
if capabilities.canShareCompany:
casbin_roles.append((user_email, f'{company}/sharing', 'Post'))
if capabilities.canAddFiscal:
casbin_roles.append((user_email, f'{company}/fiscal_year', 'POST'))
if capabilities.canAddCategory:
casbin_roles.append((user_email, f'{company}/fiscals/*/categories', 'Post'))
if capabilities.canEditCategory:
casbin_roles.append((user_email, f'{company}/fiscals/*/categories/*', 'PUT'))
if capabilities.canGetCategory:
casbin_roles.append((user_email, f'{company}/fiscals/*/categories', 'GET'))
if capabilities.canAddAccount:
casbin_roles.append((user_email, f'{company}/fiscals/*/accounts', 'POST'))
if capabilities.canGetAccount:
casbin_roles.append((user_email, f'{company}/fiscals/*/accounts', 'GET'))
if capabilities.canEditAccount:
casbin_roles.append((user_email, f'{company}/fiscals/*/accounts/*', 'PUT'))
if capabilities.canAddAccount:
casbin_roles.append((user_email, f'{company}/fiscals/*/account', 'POST'))
if capabilities.canAddTax:
casbin_roles.append((user_email, f'{company}/tax', 'POST'))
if capabilities.canAddScan:
casbin_roles.append((user_email, f'{company}/scan', 'POST'))
if capabilities.canAddTransaction:
casbin_roles.append((user_email, f'{company}/accounts/*', 'POST'))
return casbin_roles
моя попытка:
import functools
def permission_control(user_email,company):
def partial(attribute_name):
return functools.partial(getattr,capabilities,attribute_name)
capabilities = Capabilities()
permissions = ["canEditCompany","canDeleteCompany","canShareCompany","canAddFiscal","canAddCategory","canEditCategory","canGetCategory","canAddAccount","canGetAccount","canEditAccount","canAddAccount","canAddTax","canAddScan","canAddTransaction"]
urls = [(company, 'PUT'),(company, 'Delete'),(f'{company}/sharing', 'Post'),(f'{company}/fiscal_year', 'POST'),(f'{company}/fiscals/*/categories', 'Post'),(f'{company}/fiscals/*/categories/*', 'PUT'),(f'{company}/fiscals/*/categories', 'GET'),(f'{company}/fiscals/*/accounts', 'POST'),(f'{company}/fiscals/*/accounts', 'GET'),(f'{company}/fiscals/*/accounts/*', 'PUT'),(f'{company}/fiscals/*/account', 'POST'),(f'{company}/tax', 'POST'),(f'{company}/scan', 'POST'),(f'{company}/accounts/*', 'POST')]
return [((user_email,) + url) for permission,url in zip(permissions,urls) if partial(permission)()]
вопрос
- Есть ли лучший, более питонический способ добиться этого?
- Я думаю, что мой код может быть даже хуже, чем исходный код, потому что его трудно понять / поддерживать моим коллегам. Я прав?
2 ответа
Я мог бы сделать что-то вроде этого:
class Capabilities:
def __init__(self) -> None:
self.canEditCompany = True
self.canDeleteCompany = True
self.canShareCompany = True
self.canAddFiscal = True
self.canAddCategory = True
self.canEditCategory = True
self.canGetCategory = True
self.canAddAccount = True
self.canGetAccount = True
self.canEditAccount = True
self.canAddTransaction = True
self.canAddScan = True
self.canAddTax = True
actions_dict = {
"canEditCompany": lambda company: (company, 'PUT'),
"canDeleteCompany": lambda company: (company, 'Delete'),
"canShareCompany": lambda company: (f'{company}/sharing', 'Post'),
"canAddFiscal": lambda company: (f'{company}/fiscal_year', 'POST'),
"canAddCategory": lambda company: (f'{company}/fiscals/*/categories', 'Post'),
"canEditCategory": lambda company: (f'{company}/fiscals/*/categories/*', 'PUT'),
"canGetCategory": lambda company: (f'{company}/fiscals/*/categories', 'GET'),
"canAddAccount": lambda company: (f'{company}/fiscals/*/accounts', 'POST'),
"canGetAccount": lambda company: (f'{company}/fiscals/*/accounts', 'GET'),
"canEditAccount": lambda company: (f'{company}/fiscals/*/accounts/*', 'PUT'),
"canAddAccount": lambda company: (f'{company}/fiscals/*/account', 'POST'),
"canAddTax": lambda company: (f'{company}/tax', 'POST'),
"canAddScan": lambda company: (f'{company}/scan', 'POST'),
"canAddTransaction": lambda company: (f'{company}/accounts/*', 'POST')
}
def permission_control(user_email, company):
capabilities = Capabilities()
return [(user_email, *action(company)) for capability, action in actions_dict.items() if getattr(capabilities, capability)]
Я согласен с тем, что ваш код был в некотором роде менее читабельным, но я думаю, что вы двигались в правильном направлении, убрав это ужасное дерево if, которое, на мой взгляд, определенно считается анти-шаблоном. Я думаю, что хранить ввод и результат вместе в словаре гораздо удобнее, чем хранить их в двух отдельных списках, как у вас, и используя lambda
функций, мы можем создать dict в глобальном пространстве имен, чтобы его не нужно было создавать заново каждый раз при вызове функции, как ваш код делал с вашими двумя списками.
РЕДАКТИРОВАТЬ: Следуя предложению в комментариях, еще лучшим решением было бы использовать format-strings
скорее, чем lambdas
:
class Capabilities:
def __init__(self) -> None:
self.canEditCompany = True
self.canDeleteCompany = True
self.canShareCompany = True
self.canAddFiscal = True
self.canAddCategory = True
self.canEditCategory = True
self.canGetCategory = True
self.canAddAccount = True
self.canGetAccount = True
self.canEditAccount = True
self.canAddTransaction = True
self.canAddScan = True
self.canAddTax = True
actions_dict = {
"canEditCompany": ('{}', 'PUT'),
"canDeleteCompany": ('{}', 'Delete'),
"canShareCompany": ('{}/sharing', 'Post'),
"canAddFiscal": ('{}/fiscal_year', 'POST'),
"canAddCategory": ('{}/fiscals/*/categories', 'Post'),
"canEditCategory": ('{}/fiscals/*/categories/*', 'PUT'),
"canGetCategory": ('{}/fiscals/*/categories', 'GET'),
"canAddAccount": ('{}/fiscals/*/accounts', 'POST'),
"canGetAccount": ('{}/fiscals/*/accounts', 'GET'),
"canEditAccount": ('{}/fiscals/*/accounts/*', 'PUT'),
"canAddAccount": ('{}/fiscals/*/account', 'POST'),
"canAddTax": ('{}/tax', 'POST'),
"canAddScan": ('{}/scan', 'POST'),
"canAddTransaction": ('{}/accounts/*', 'POST')
}
def permission_control(user_email, company):
capabilities = Capabilities()
return [
(user_email, url.format(company), method)
for capability, (url, method) in actions_dict.items()
if getattr(capabilities, capability)
]
ВТОРОЕ РЕДАКТИРОВАНИЕ: как было указано в комментариях, ответ со строками формата можно улучшить еще больше, предварительно привязав format
метод форматных строк. Это немного более производительно и ограничивает струны одной целью.
Я также изменил структуру данных с str: tuple[str, str]
словарь в список NamedTuples
. Словарь на этом этапе терял смысл. NamedTuples не являются строго обязательными в отличие от кортежей, но я думаю, что их использование делает намерение кода более ясным и улучшает читаемость.
from typing import NamedTuple, Callable, Literal
class Capabilities:
def __init__(self) -> None:
self.canEditCompany = True
self.canDeleteCompany = True
self.canShareCompany = True
self.canAddFiscal = True
self.canAddCategory = True
self.canEditCategory = True
self.canGetCategory = True
self.canAddAccount = True
self.canGetAccount = True
self.canEditAccount = True
self.canAddTransaction = True
self.canAddScan = True
self.canAddTax = True
class CapabilityTuple(NamedTuple):
capability: str
url_factory: Callable[[str], str]
# Any reason why some of these aren't all-uppercase in your question?
# HTTP methods are usually all-uppercase
method: Literal['PUT', 'Delete', 'POST', 'GET', 'Post']
actions_list = [
CapabilityTuple(capability, url.format, method) for capability, url, method in (
("canEditCompany", '{}', 'PUT'),
("canDeleteCompany", '{}', 'Delete'),
("canShareCompany", '{}/sharing', 'Post'),
("canAddFiscal", '{}/fiscal_year', 'POST'),
("canAddCategory", '{}/fiscals/*/categories', 'Post'),
("canEditCategory", '{}/fiscals/*/categories/*', 'PUT'),
("canGetCategory", '{}/fiscals/*/categories', 'GET'),
("canAddAccount", '{}/fiscals/*/accounts', 'POST'),
("canGetAccount", '{}/fiscals/*/accounts', 'GET'),
("canEditAccount", '{}/fiscals/*/accounts/*', 'PUT'),
("canAddAccount", '{}/fiscals/*/account', 'POST'),
("canAddTax", '{}/tax', 'POST'),
("canAddScan", '{}/scan', 'POST'),
("canAddTransaction", '{}/accounts/*', 'POST')
)
]
def permission_control(user_email, company):
capabilities = Capabilities()
return [
(user_email, url_factory(company), method)
for capability, url_factory, method in actions_list
if getattr(capabilities, capability)
]
Я бы немного потупил: без лямбд, без партиалов, без форматирования (у вас есть только суффиксы и ничего больше). Сделайте свои методы HTTP заглавными для единообразия, а также вы можете использовать генератор вместо понимания списка. Для тебя Capabilities
вы можете принимать логические kwargs. Предварительно зарегистрируйте все возможные возможности в постоянном словаре, примерно похожем на словарь Алекса. actions_dict
но с более сильным набором текста.
Также обратите внимание permission_control
это существительное, в котором имена методов должны быть глаголами, т.е. control_permissions
или, как я предложил, .evaluate()
Предложенный
from dataclasses import dataclass
from typing import Tuple, Iterable
CapTuple = Tuple[
str, # email
str, # URL path
str, # method
]
@dataclass(frozen=True)
class Capability:
name: str
method: str
path: str
CAPABILITIES = {
name: Capability(name, method, path)
for name, method, path in (
("canEditCompany", 'PUT', ''),
("canDeleteCompany", 'DELETE', ''),
("canShareCompany", 'POST', '/sharing'),
("canAddFiscal", 'POST', '/fiscal_year'),
("canAddCategory", 'POST', '/fiscals/*/categories'),
("canEditCategory", 'PUT', '/fiscals/*/categories/*'),
("canGetCategory", 'GET', '/fiscals/*/categories'),
("canAddAccount", 'POST', '/fiscals/*/accounts'),
("canGetAccount", 'GET', '/fiscals/*/accounts'),
("canEditAccount", 'PUT', '/fiscals/*/accounts/*'),
("canAddAccount", 'POST', '/fiscals/*/account'),
("canAddTax", 'POST', '/tax'),
("canAddScan", 'POST', '/scan'),
("canAddTransaction", 'POST', '/accounts/*'),
)
}
class Capabilities:
def __init__(self, **kwargs: bool):
self.allowed: Tuple[Capability] = tuple(
CAPABILITIES[name]
for name, allowed in kwargs.items()
if allowed
)
def evaluate(self, user_email: str, company: str) -> Iterable[CapTuple]:
for cap in self.allowed:
yield user_email, company + cap.path, cap.method
def test() -> None:
caps = Capabilities(canAddAccount=True, canEditAccount=True, canDeleteCompany=False)
for user_email, path, method in caps.evaluate('me@here.com', 'here_inc'):
print(user_email, path, method)
if __name__ == '__main__':
test()
Это полезный ответ. Одна альтернатива для рассмотрения: строки формата, которые все еще можно определить заранее, а не лямбда-выражения, которые немного тяжелее с точки зрения синтаксиса.
— FMc
О, отличная идея! Я включил это в свой ответ — это делает его намного чище
— Алекс Вэйгуд
да точно. Повышение производительности будет очень незначительным, но, что более важно, строка ограничена только одной целью.
— Райндериен
Честно говоря, это потому, что я не знал о новом (иш)
NamedTuple
определение стиля наследования. Это приятно — спасибо, что показали это.— Райндериен
Да, мне это нравится гораздо больше — позволяет использовать подсказки типов, а также упрощает расширение класса и добавление ваших собственных методов — например, github.com/willmcgugan/textual/blob/main/src/textual/…. Мне всегда кажется странным, что он находится в модуле набора текста, а не в коллекциях.
— Алекс Вэйгуд
Показать 5 больше комментариев