Skip to content
This repository was archived by the owner on Jun 1, 2023. It is now read-only.

Commit 7a6cb5a

Browse files
authored
Merge pull request #40 from IdentityPython/develop
Allow class attribute to be either dictionary of dictionary like class instance
2 parents 7193220 + 93ff90d commit 7a6cb5a

File tree

8 files changed

+470
-5
lines changed

8 files changed

+470
-5
lines changed

Diff for: setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def run_tests(self):
5151
author_email="[email protected]",
5252
license="Apache 2.0",
5353
url='https://github.com/IdentityPython/oidcmsg/',
54-
packages=["oidcmsg", "oidcmsg/oauth2", "oidcmsg/oidc"],
54+
packages=["oidcmsg", "oidcmsg/oauth2", "oidcmsg/oidc", "oidcmsg/storage"],
5555
package_dir={"": "src"},
5656
classifiers=[
5757
"Development Status :: 4 - Beta",

Diff for: src/oidcmsg/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
__author__ = "Roland Hedberg"
2-
__version__ = "1.3.3"
2+
__version__ = "1.4.0"
33

44
import os
55
from typing import Dict

Diff for: src/oidcmsg/context.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,14 @@ def __init__(self, config=None, keyjar=None, entity_id=""):
2727
ImpExp.__init__(self)
2828
if config is None:
2929
config = {}
30-
3130
self.issuer = entity_id
3231
self.keyjar = self._keyjar(keyjar, conf=config, entity_id=entity_id)
3332

3433
def _keyjar(self, keyjar=None, conf=None, entity_id=""):
3534
if keyjar is None:
3635
if "keys" in conf:
37-
args = {k: v for k, v in conf["keys"].items() if k != "uri_path"}
38-
_keyjar = init_key_jar(**args)
36+
keys_args = {k: v for k, v in conf["keys"].items() if k != "uri_path"}
37+
_keyjar = init_key_jar(**keys_args)
3938
else:
4039
_keyjar = KeyJar()
4140
if "jwks" in conf:

Diff for: src/oidcmsg/impexp.py

+20
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
from cryptojwt.utils import qualified_name
88

99
from oidcmsg.message import Message
10+
from oidcmsg.storage import DictType
11+
12+
13+
def fully_qualified_name(cls):
14+
return cls.__module__ + "." + cls.__class__.__name__
1015

1116

1217
class ImpExp:
@@ -23,6 +28,15 @@ def dump_attr(self, cls, item, exclude_attributes: Optional[List[str]] = None) -
2328
val = as_bytes(item)
2429
else:
2530
val = item
31+
elif cls == "DICT_TYPE":
32+
if isinstance(item, dict):
33+
val = item
34+
else:
35+
if isinstance(item, DictType): # item should be a class instance
36+
val = {
37+
"DICT_TYPE": {"class": fully_qualified_name(item), "kwargs": item.kwargs}}
38+
else:
39+
raise ValueError("Expected a DictType class")
2640
elif isinstance(item, Message):
2741
val = {qualified_name(item.__class__): item.to_dict()}
2842
elif cls == object:
@@ -83,6 +97,12 @@ def load_attr(
8397
val = as_bytes(item)
8498
else:
8599
val = item
100+
elif cls == "DICT_TYPE":
101+
if list(item.keys()) == ["DICT_TYPE"]:
102+
_spec = item["DICT_TYPE"]
103+
val = importer(_spec["class"])(**_spec["kwargs"])
104+
else:
105+
val = item
86106
elif cls == object:
87107
val = importer(item)
88108
elif isinstance(cls, list):

Diff for: src/oidcmsg/storage/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class DictType(object):
2+
def __init__(self, **kwargs):
3+
self.kwargs = kwargs

Diff for: src/oidcmsg/storage/abfile.py

+291
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
import logging
2+
import os
3+
import time
4+
from typing import Optional
5+
6+
from cryptojwt.utils import importer
7+
from filelock import FileLock
8+
9+
from oidcmsg.storage import DictType
10+
from oidcmsg.util import PassThru
11+
from oidcmsg.util import QPKey
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
class AbstractFileSystem(DictType):
17+
"""
18+
FileSystem implements a simple file based database.
19+
It has a dictionary like interface.
20+
Each key maps one-to-one to a file on disc, where the content of the
21+
file is the value.
22+
ONLY goes one level deep.
23+
Not directories in directories.
24+
"""
25+
26+
def __init__(self,
27+
fdir: Optional[str] = '',
28+
key_conv: Optional[str] = '',
29+
value_conv: Optional[str] = ''):
30+
"""
31+
items = FileSystem(
32+
{
33+
'fdir': fdir,
34+
'key_conv':{'to': quote_plus, 'from': unquote_plus},
35+
'value_conv':{'to': keyjar_to_jwks, 'from': jwks_to_keyjar}
36+
})
37+
38+
:param fdir: The root of the directory
39+
:param key_conv: Converts to/from the key displayed by this class to
40+
users of it to something that can be used as a file name.
41+
The value of key_conv is a class that has the methods 'serialize'/'deserialize'.
42+
:param value_conv: As with key_conv you can convert/translate
43+
the value bound to a key in the database to something that can easily
44+
be stored in a file. Like with key_conv the value of this parameter
45+
is a class that has the methods 'serialize'/'deserialize'.
46+
"""
47+
super(AbstractFileSystem, self).__init__(fdir=fdir, key_conv=key_conv, value_conv=value_conv)
48+
49+
self.fdir = fdir
50+
self.fmtime = {}
51+
self.storage = {}
52+
53+
if key_conv:
54+
self.key_conv = importer(key_conv)()
55+
else:
56+
self.key_conv = QPKey()
57+
58+
if value_conv:
59+
self.value_conv = importer(value_conv)()
60+
else:
61+
self.value_conv = PassThru()
62+
63+
if not os.path.isdir(self.fdir):
64+
os.makedirs(self.fdir)
65+
66+
self.synch()
67+
68+
def get(self, item, default=None):
69+
try:
70+
return self[item]
71+
except KeyError:
72+
return default
73+
74+
def __getitem__(self, item):
75+
"""
76+
Return the value bound to an identifier.
77+
78+
:param item: The identifier.
79+
:return:
80+
"""
81+
item = self.key_conv.serialize(item)
82+
83+
if self.is_changed(item):
84+
logger.info("File content change in {}".format(item))
85+
fname = os.path.join(self.fdir, item)
86+
self.storage[item] = self._read_info(fname)
87+
88+
logger.debug('Read from "%s"', item)
89+
return self.storage[item]
90+
91+
def __setitem__(self, key, value):
92+
"""
93+
Binds a value to a specific key. If the file that the key maps to
94+
does not exist it will be created. The content of the file will be
95+
set to the value given.
96+
97+
:param key: Identifier
98+
:param value: Value that should be bound to the identifier.
99+
:return:
100+
"""
101+
102+
if not os.path.isdir(self.fdir):
103+
os.makedirs(self.fdir, exist_ok=True)
104+
105+
try:
106+
_key = self.key_conv.serialize(key)
107+
except KeyError:
108+
_key = key
109+
110+
fname = os.path.join(self.fdir, _key)
111+
lock = FileLock('{}.lock'.format(fname))
112+
with lock:
113+
with open(fname, 'w') as fp:
114+
fp.write(self.value_conv.serialize(value))
115+
116+
self.storage[_key] = value
117+
logger.debug('Wrote to "%s"', key)
118+
self.fmtime[_key] = self.get_mtime(fname)
119+
120+
def __delitem__(self, key):
121+
fname = os.path.join(self.fdir, key)
122+
if os.path.isfile(fname):
123+
lock = FileLock('{}.lock'.format(fname))
124+
with lock:
125+
os.unlink(fname)
126+
127+
try:
128+
del self.storage[key]
129+
except KeyError:
130+
pass
131+
132+
def keys(self):
133+
"""
134+
Implements the dict.keys() method
135+
"""
136+
self.synch()
137+
for k in self.storage.keys():
138+
yield self.key_conv.deserialize(k)
139+
140+
@staticmethod
141+
def get_mtime(fname):
142+
"""
143+
Find the time this file was last modified.
144+
145+
:param fname: File name
146+
:return: The last time the file was modified.
147+
"""
148+
try:
149+
mtime = os.stat(fname).st_mtime_ns
150+
except OSError:
151+
# The file might be right in the middle of being written
152+
# so sleep
153+
time.sleep(1)
154+
mtime = os.stat(fname).st_mtime_ns
155+
156+
return mtime
157+
158+
def is_changed(self, item):
159+
"""
160+
Find out if this item has been modified since last
161+
162+
:param item: A key
163+
:return: True/False
164+
"""
165+
fname = os.path.join(self.fdir, item)
166+
if os.path.isfile(fname):
167+
mtime = self.get_mtime(fname)
168+
169+
try:
170+
_ftime = self.fmtime[item]
171+
except KeyError: # Never been seen before
172+
self.fmtime[item] = mtime
173+
return True
174+
175+
if mtime > _ftime: # has changed
176+
self.fmtime[item] = mtime
177+
return True
178+
else:
179+
return False
180+
else:
181+
logger.error('Could not access {}'.format(fname))
182+
raise KeyError(item)
183+
184+
def _read_info(self, fname):
185+
if os.path.isfile(fname):
186+
try:
187+
lock = FileLock('{}.lock'.format(fname))
188+
with lock:
189+
info = open(fname, 'r').read().strip()
190+
return self.value_conv.deserialize(info)
191+
except Exception as err:
192+
logger.error(err)
193+
raise
194+
else:
195+
logger.error('No such file: {}'.format(fname))
196+
return None
197+
198+
def synch(self):
199+
"""
200+
Goes through the directory and builds a local cache based on
201+
the content of the directory.
202+
"""
203+
if not os.path.isdir(self.fdir):
204+
os.makedirs(self.fdir)
205+
# raise ValueError('No such directory: {}'.format(self.fdir))
206+
for f in os.listdir(self.fdir):
207+
fname = os.path.join(self.fdir, f)
208+
209+
if not os.path.isfile(fname):
210+
continue
211+
if fname.endswith('.lock'):
212+
continue
213+
214+
if f in self.fmtime:
215+
if self.is_changed(f):
216+
self.storage[f] = self._read_info(fname)
217+
else:
218+
mtime = self.get_mtime(fname)
219+
try:
220+
self.storage[f] = self._read_info(fname)
221+
except Exception as err:
222+
logger.warning('Bad content in {} ({})'.format(fname, err))
223+
else:
224+
self.fmtime[f] = mtime
225+
226+
def items(self):
227+
"""
228+
Implements the dict.items() method
229+
"""
230+
self.synch()
231+
for k, v in self.storage.items():
232+
yield self.key_conv.deserialize(k), v
233+
234+
def clear(self):
235+
"""
236+
Completely resets the database. This means that all information in
237+
the local cache and on disc will be erased.
238+
"""
239+
if not os.path.isdir(self.fdir):
240+
os.makedirs(self.fdir, exist_ok=True)
241+
return
242+
243+
for f in os.listdir(self.fdir):
244+
del self[f]
245+
246+
def update(self, ava):
247+
"""
248+
Replaces what's in the database with a set of key, value pairs.
249+
Only data bound to keys that appear in ava will be affected.
250+
251+
Implements the dict.update() method
252+
253+
:param ava: Dictionary
254+
"""
255+
for key, val in ava.items():
256+
self[key] = val
257+
258+
def __contains__(self, item):
259+
return self.key_conv.serialize(item) in self.storage
260+
261+
def __iter__(self):
262+
return self.items()
263+
264+
def __call__(self, *args, **kwargs):
265+
return [self.key_conv.deserialize(k) for k in self.storage.keys()]
266+
267+
def __len__(self):
268+
if not os.path.isdir(self.fdir):
269+
return 0
270+
271+
n = 0
272+
for f in os.listdir(self.fdir):
273+
fname = os.path.join(self.fdir, f)
274+
275+
if not os.path.isfile(fname):
276+
continue
277+
if fname.endswith('.lock'):
278+
continue
279+
280+
n += 1
281+
return n
282+
283+
def __str__(self):
284+
return '{config:' + str(self.config) + ', info:' + str(self.storage) + '}'
285+
286+
def dump(self):
287+
return {k: v for k, v in self.items()}
288+
289+
def load(self, info):
290+
for k, v in info.items():
291+
self[k] = v

0 commit comments

Comments
 (0)