Skip to content

Commit d557189

Browse files
author
Elias Gabriel
authored
feat: add interface stubs for async adapters (#335)
* refactor!: change reorganize imports for sane asyncio ext * feat: async adapter interface stubs * chore: reflect pr comments * chore: revert black bump pending decision * refactor: move update adapter interface to persist module * ci: bump tooling and linting versions * ci: fix linting action
1 parent 47e5ef5 commit d557189

23 files changed

Lines changed: 360 additions & 238 deletions

.github/workflows/build.yml

Lines changed: 43 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -11,55 +11,53 @@ jobs:
1111
strategy:
1212
fail-fast: false
1313
matrix:
14-
python-version: ['3.9', '3.10', '3.11']
14+
python-version: ["3.9", "3.10", "3.11"]
1515
os: [ubuntu-latest, macOS-latest, windows-latest]
1616

1717
steps:
18-
- name: Checkout
19-
uses: actions/checkout@v2
18+
- name: Checkout
19+
uses: actions/checkout@v4
2020

21-
- name: Set up Python ${{ matrix.python-version }}
22-
uses: actions/setup-python@v2
23-
with:
24-
python-version: ${{ matrix.python-version }}
21+
- name: Set up Python ${{ matrix.python-version }}
22+
uses: actions/setup-python@v5
23+
with:
24+
python-version: ${{ matrix.python-version }}
2525

26-
- name: Install dependencies
27-
run: |
28-
pip install -r requirements.txt
29-
pip install -r requirements_dev.txt
30-
pip install coveralls
31-
pip install pytest
32-
pip install pytest-benchmark
26+
- name: Install dependencies
27+
run: |
28+
pip install -r requirements.txt
29+
pip install -r requirements_dev.txt
30+
pip install coveralls
3331
34-
- name: Run tests
35-
run: coverage run -m unittest discover -s tests -t tests
32+
- name: Run tests
33+
run: coverage run -m unittest discover -s tests -t tests
3634

37-
- name: Run benchmark
38-
run: python3 -m pytest
39-
--benchmark-verbose
40-
--benchmark-columns=mean,stddev,iqr,ops,rounds
41-
tests/benchmarks/benchmark_model.py
42-
tests/benchmarks/benchmark_management_api.py
43-
tests/benchmarks/benchmark_role_manager.py
35+
- name: Run benchmark
36+
run: python3 -m pytest
37+
--benchmark-verbose
38+
--benchmark-columns=mean,stddev,iqr,ops,rounds
39+
tests/benchmarks/benchmark_model.py
40+
tests/benchmarks/benchmark_management_api.py
41+
tests/benchmarks/benchmark_role_manager.py
4442

45-
- name: Upload coverage data to coveralls.io
46-
run: coveralls --service=github
47-
env:
48-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49-
COVERALLS_FLAG_NAME: ${{ matrix.os }} - ${{ matrix.python-version }}
50-
COVERALLS_PARALLEL: true
43+
- name: Upload coverage data to coveralls.io
44+
run: coveralls --service=github
45+
env:
46+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47+
COVERALLS_FLAG_NAME: ${{ matrix.os }} - ${{ matrix.python-version }}
48+
COVERALLS_PARALLEL: true
5149

5250
lint:
5351
name: Run Linters
5452
runs-on: ubuntu-latest
5553
steps:
5654
- name: Checkout
57-
uses: actions/checkout@v2
55+
uses: actions/checkout@v4
5856
with:
5957
fetch-depth: 0
6058

6159
- name: Super-Linter
62-
uses: github/super-linter@v4.9.2
60+
uses: super-linter/super-linter@v5.7.2
6361
env:
6462
VALIDATE_ALL_CODEBASE: false
6563
VALIDATE_PYTHON_BLACK: true
@@ -74,36 +72,36 @@ jobs:
7472
runs-on: ubuntu-latest
7573
container: python:3-slim
7674
steps:
77-
- name: Finished
78-
run: |
79-
pip3 install --upgrade coveralls
80-
coveralls --finish
81-
env:
82-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
75+
- name: Finished
76+
run: |
77+
pip3 install --upgrade coveralls
78+
coveralls --finish
79+
env:
80+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
8381

8482
release:
8583
name: Release
8684
runs-on: ubuntu-latest
87-
needs: [ test, coveralls ]
85+
needs: [test, coveralls]
8886
steps:
8987
- name: Checkout
90-
uses: actions/checkout@v2
88+
uses: actions/checkout@v4
9189
with:
9290
fetch-depth: 0
93-
91+
9492
- name: Setup Node.js
95-
uses: actions/setup-node@v2
93+
uses: actions/setup-node@v4
9694
with:
97-
node-version: '18'
95+
node-version: "18"
9896

9997
- name: Setup
10098
run: npm install
101-
99+
102100
- name: Set up python
103-
uses: actions/setup-python@v4
101+
uses: actions/setup-python@v5
104102
with:
105103
python-version: 3.11
106-
104+
107105
- name: Release
108106
env:
109107
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ https://casbin.org/docs/role-managers
232232

233233
If your code use `async` / `await` and is heavily dependent on I/O operations, you can adopt Async Enforcer!
234234

235-
1. Create an async engine and new a Casbin AsyncEnforcer with a model file and an async Pycasbin adapter:
235+
1. Create an async engine and new a Casbin AsyncEnforcer with a model file and an async Pycasbin adapter (AsyncAdapter subclass):
236236

237237
```python
238238
import asyncio
@@ -266,6 +266,8 @@ async def get_enforcer():
266266

267267
Note: you can see all supported adapters in [Adapters | Casbin](https://casbin.org/docs/adapters).
268268

269+
Built-in async adapters are available in `casbin.persist.adapters.asyncio`.
270+
269271
2. Add an enforcement hook into your code right before the access happens:
270272

271273
```python

casbin/async_internal_enforcer.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515

1616
from casbin.core_enforcer import CoreEnforcer
1717
from casbin.model import Model, FunctionMap
18-
from casbin.persist import Adapter
19-
from casbin.persist.adapters.async_file_adapter import AsyncFileAdapter
18+
from casbin.persist.adapters.asyncio import AsyncFileAdapter, AsyncAdapter
2019

2120

2221
class AsyncInternalEnforcer(CoreEnforcer):
@@ -32,7 +31,7 @@ def init_with_file(self, model_path, policy_path):
3231
def init_with_model_and_adapter(self, m, adapter=None):
3332
"""initializes an enforcer with a model and a database adapter."""
3433

35-
if not isinstance(m, Model) or adapter is not None and not isinstance(adapter, Adapter):
34+
if not isinstance(m, Model) or adapter is not None and not isinstance(adapter, AsyncAdapter):
3635
raise RuntimeError("Invalid parameters for enforcer.")
3736

3837
self.adapter = adapter

casbin/config/config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ def _parse_buffer(self, f):
9696
buf.append(p)
9797

9898
def _write(self, section, line_num, b):
99-
10099
buf = "".join(b)
101100
if len(buf) <= 0:
102101
return

casbin/distributed_enforcer.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@
1313
# limitations under the License.
1414

1515
from casbin.model.policy_op import PolicyOp
16-
from casbin.persist import batch_adapter
17-
from casbin.persist.adapters import update_adapter
16+
from casbin.persist import batch_adapter, update_adapter
1817
from casbin.synced_enforcer import SyncedEnforcer
1918

2019

casbin/persist/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@
1414

1515
from .adapter import *
1616
from .adapter_filtered import *
17-
from .batch_adapter import *
1817
from .adapters import *
18+
from .batch_adapter import *

casbin/persist/adapters/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,10 @@
1313
# limitations under the License.
1414

1515
from .file_adapter import FileAdapter
16-
from .adapter_filtered import FilteredAdapter
17-
from .update_adapter import UpdateAdapter
16+
from .filtered_file_adapter import FilteredFileAdapter
17+
from ..update_adapter import UpdateAdapter
18+
19+
# alias import for backwards compatibility
20+
FilteredAdapter = FilteredFileAdapter
21+
22+
__all__ = ["FileAdapter", "FilteredFileAdapter", "FilteredAdapter", "UpdateAdapter"]
Lines changed: 5 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,7 @@
1-
# Copyright 2021 The casbin Authors. All Rights Reserved.
2-
#
3-
# Licensed under the Apache License, Version 2.0 (the "License");
4-
# you may not use this file except in compliance with the License.
5-
# You may obtain a copy of the License at
6-
#
7-
# http://www.apache.org/licenses/LICENSE-2.0
8-
#
9-
# Unless required by applicable law or agreed to in writing, software
10-
# distributed under the License is distributed on an "AS IS" BASIS,
11-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12-
# See the License for the specific language governing permissions and
13-
# limitations under the License.
1+
# NOTE: this file exists as a backwards compatible alias. please directly
2+
# use FilteredFileAdapter from `casbin.persist.adapters.filtered_file_adapter` instead.
143

15-
from casbin import persist
16-
from .file_adapter import FileAdapter
17-
import os
4+
from .filtered_file_adapter import Filter
5+
from .filtered_file_adapter import FilteredFileAdapter as FilteredAdapter
186

19-
20-
class Filter:
21-
# P,G are string []
22-
P = []
23-
G = []
24-
25-
26-
class FilteredAdapter(FileAdapter, persist.FilteredAdapter):
27-
filtered = False
28-
_file_path = ""
29-
filter = Filter()
30-
# new_filtered_adapte is the constructor for FilteredAdapter.
31-
def __init__(self, file_path):
32-
self.filtered = True
33-
self._file_path = file_path
34-
35-
def load_policy(self, model):
36-
if not os.path.isfile(self._file_path):
37-
raise RuntimeError("invalid file path, file path cannot be empty")
38-
self.filtered = False
39-
self._load_policy_file(model)
40-
41-
# load_filtered_policy loads only policy rules that match the filter.
42-
def load_filtered_policy(self, model, filter):
43-
if filter == None:
44-
return self.load_policy(model)
45-
46-
if not os.path.isfile(self._file_path):
47-
raise RuntimeError("invalid file path, file path cannot be empty")
48-
49-
try:
50-
filter_value = [filter.__dict__["P"]] + [filter.__dict__["G"]]
51-
except:
52-
raise RuntimeError("invalid filter type")
53-
54-
self.load_filtered_policy_file(model, filter_value, persist.load_policy_line)
55-
self.filtered = True
56-
57-
def load_filtered_policy_file(self, model, filter, hanlder):
58-
with open(self._file_path, "rb") as file:
59-
while True:
60-
line = file.readline()
61-
line = line.decode().strip()
62-
if line == "\n":
63-
continue
64-
if not line:
65-
break
66-
if filter_line(line, filter):
67-
continue
68-
69-
hanlder(line, model)
70-
71-
# is_filtered returns true if the loaded policy has been filtered.
72-
def is_filtered(self):
73-
return self.filtered
74-
75-
def save_policy(self, model):
76-
if self.filtered:
77-
raise RuntimeError("cannot save a filtered policy")
78-
79-
self._save_policy_file(model)
80-
81-
82-
def filter_line(line, filter):
83-
if filter == None:
84-
return False
85-
86-
p = line.split(",")
87-
if len(p) == 0:
88-
return True
89-
filter_slice = []
90-
91-
if p[0].strip() == "p":
92-
filter_slice = filter[0]
93-
elif p[0].strip() == "g":
94-
filter_slice = filter[1]
95-
return filter_words(p, filter_slice)
96-
97-
98-
def filter_words(line, filter):
99-
if len(line) < len(filter) + 1:
100-
return True
101-
skip_line = False
102-
for i, v in enumerate(filter):
103-
if len(v) > 0 and (v.strip() != line[i + 1].strip()):
104-
skip_line = True
105-
break
106-
107-
return skip_line
7+
__all__ = ["Filter", "FilteredAdapter"]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from .adapter import AsyncAdapter
2+
from .adapter_filtered import AsyncFilteredAdapter
3+
from .batch_adapter import AsyncBatchAdapter
4+
from .file_adapter import AsyncFileAdapter
5+
from .update_adapter import AsyncUpdateAdapter
6+
7+
__all__ = [
8+
"AsyncAdapter",
9+
"AsyncFilteredAdapter",
10+
"AsyncBatchAdapter",
11+
"AsyncFileAdapter",
12+
"AsyncUpdateAdapter",
13+
]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from abc import ABCMeta, abstractmethod
2+
3+
4+
class AsyncAdapter(metaclass=ABCMeta):
5+
"""The interface for async Casbin adapters."""
6+
7+
@abstractmethod
8+
async def load_policy(self, model):
9+
"""loads all policy rules from the storage."""
10+
pass
11+
12+
@abstractmethod
13+
async def save_policy(self, model):
14+
"""saves all policy rules to the storage."""
15+
pass
16+
17+
@abstractmethod
18+
async def add_policy(self, sec, ptype, rule):
19+
"""adds a policy rule to the storage."""
20+
pass
21+
22+
@abstractmethod
23+
async def remove_policy(self, sec, ptype, rule):
24+
"""removes a policy rule from the storage."""
25+
pass
26+
27+
@abstractmethod
28+
async def remove_filtered_policy(self, sec, ptype, field_index, *field_values):
29+
"""removes policy rules that match the filter from the storage.
30+
This is part of the Auto-Save feature.
31+
"""
32+
pass

0 commit comments

Comments
 (0)