zope.locking 3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zope/locking/README.rst +1070 -0
- zope/locking/__init__.py +1 -0
- zope/locking/adapters.py +174 -0
- zope/locking/annoying.rst +353 -0
- zope/locking/cleanup.rst +457 -0
- zope/locking/configure.zcml +9 -0
- zope/locking/ftesting.zcml +12 -0
- zope/locking/generations.py +97 -0
- zope/locking/generations.zcml +11 -0
- zope/locking/interfaces.py +462 -0
- zope/locking/testing.py +64 -0
- zope/locking/tests.py +67 -0
- zope/locking/tokens.py +256 -0
- zope/locking/utility.py +149 -0
- zope/locking/utils.py +23 -0
- zope.locking-3.0-py3.12-nspkg.pth +1 -0
- zope_locking-3.0.dist-info/LICENSE.rst +44 -0
- zope_locking-3.0.dist-info/METADATA +1246 -0
- zope_locking-3.0.dist-info/RECORD +22 -0
- zope_locking-3.0.dist-info/WHEEL +5 -0
- zope_locking-3.0.dist-info/namespace_packages.txt +1 -0
- zope_locking-3.0.dist-info/top_level.txt +1 -0
zope/locking/tokens.py
ADDED
@@ -0,0 +1,256 @@
|
|
1
|
+
#############################################################################
|
2
|
+
#
|
3
|
+
# Copyright (c) 2018 Zope Foundation and Contributors.
|
4
|
+
# All Rights Reserved.
|
5
|
+
#
|
6
|
+
# This software is subject to the provisions of the Zope Public License,
|
7
|
+
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
8
|
+
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
9
|
+
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
10
|
+
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
11
|
+
# FOR A PARTICULAR PURPOSE.
|
12
|
+
#
|
13
|
+
##############################################################################
|
14
|
+
|
15
|
+
import datetime
|
16
|
+
import functools
|
17
|
+
|
18
|
+
import persistent
|
19
|
+
from BTrees.OOBTree import OOBTree
|
20
|
+
|
21
|
+
from zope import event
|
22
|
+
from zope import interface
|
23
|
+
from zope.locking import interfaces
|
24
|
+
from zope.locking import utils
|
25
|
+
|
26
|
+
|
27
|
+
NO_DURATION = datetime.timedelta()
|
28
|
+
|
29
|
+
|
30
|
+
class AnnotationsMapping(OOBTree):
|
31
|
+
"""a class on which security settings may be hung."""
|
32
|
+
|
33
|
+
|
34
|
+
@functools.total_ordering
|
35
|
+
class Token(persistent.Persistent):
|
36
|
+
|
37
|
+
def __init__(self, target):
|
38
|
+
self.context = self.__parent__ = target
|
39
|
+
self.annotations = AnnotationsMapping()
|
40
|
+
self.annotations.__parent__ = self # for security.
|
41
|
+
|
42
|
+
_principal_ids = frozenset()
|
43
|
+
|
44
|
+
@property
|
45
|
+
def principal_ids(self):
|
46
|
+
return self._principal_ids
|
47
|
+
|
48
|
+
_started = None
|
49
|
+
|
50
|
+
@property
|
51
|
+
def started(self):
|
52
|
+
if self._utility is None:
|
53
|
+
raise interfaces.UnregisteredError(self)
|
54
|
+
return self._started
|
55
|
+
|
56
|
+
_utility = None
|
57
|
+
|
58
|
+
@property
|
59
|
+
def utility(self):
|
60
|
+
return self._utility
|
61
|
+
|
62
|
+
@utility.setter
|
63
|
+
def utility(self, value):
|
64
|
+
if self._utility is not None:
|
65
|
+
if value is not self._utility:
|
66
|
+
raise ValueError('cannot reset utility')
|
67
|
+
else:
|
68
|
+
assert interfaces.ITokenUtility.providedBy(value)
|
69
|
+
self._utility = value
|
70
|
+
assert self._started is None
|
71
|
+
self._started = utils.now()
|
72
|
+
|
73
|
+
def __eq__(self, other):
|
74
|
+
return (
|
75
|
+
(self._p_jar.db().database_name, self._p_oid) ==
|
76
|
+
(other._p_jar.db().database_name, other._p_oid))
|
77
|
+
|
78
|
+
def __lt__(self, other):
|
79
|
+
return (
|
80
|
+
(self._p_jar.db().database_name, self._p_oid) <
|
81
|
+
(other._p_jar.db().database_name, other._p_oid))
|
82
|
+
|
83
|
+
|
84
|
+
class EndableToken(Token):
|
85
|
+
|
86
|
+
def __init__(self, target, duration=None):
|
87
|
+
super().__init__(target)
|
88
|
+
self._duration = duration
|
89
|
+
|
90
|
+
@property
|
91
|
+
def utility(self):
|
92
|
+
return self._utility
|
93
|
+
|
94
|
+
@utility.setter
|
95
|
+
def utility(self, value):
|
96
|
+
if self._utility is not None:
|
97
|
+
if value is not self._utility:
|
98
|
+
raise ValueError('cannot reset utility')
|
99
|
+
else:
|
100
|
+
assert interfaces.ITokenUtility.providedBy(value)
|
101
|
+
self._utility = value
|
102
|
+
assert self._started is None
|
103
|
+
self._started = utils.now()
|
104
|
+
if self._duration is not None:
|
105
|
+
self._expiration = self._started + self._duration
|
106
|
+
del self._duration # to catch bugs.
|
107
|
+
|
108
|
+
_expiration = _duration = None
|
109
|
+
|
110
|
+
@property
|
111
|
+
def expiration(self):
|
112
|
+
if self._started is None:
|
113
|
+
raise interfaces.UnregisteredError(self)
|
114
|
+
return self._expiration
|
115
|
+
|
116
|
+
@expiration.setter
|
117
|
+
def expiration(self, value):
|
118
|
+
if self._started is None:
|
119
|
+
raise interfaces.UnregisteredError(self)
|
120
|
+
if self.ended:
|
121
|
+
raise interfaces.EndedError
|
122
|
+
if value is not None:
|
123
|
+
if not isinstance(value, datetime.datetime):
|
124
|
+
raise ValueError('expiration must be datetime.datetime')
|
125
|
+
elif value.tzinfo is None:
|
126
|
+
raise ValueError('expiration must be timezone-aware')
|
127
|
+
old = self._expiration
|
128
|
+
self._expiration = value
|
129
|
+
if old != self._expiration:
|
130
|
+
self.utility.register(self)
|
131
|
+
event.notify(interfaces.ExpirationChangedEvent(self, old))
|
132
|
+
|
133
|
+
@property
|
134
|
+
def duration(self):
|
135
|
+
if self._started is None:
|
136
|
+
return self._duration
|
137
|
+
if self._expiration is None:
|
138
|
+
return None
|
139
|
+
return self._expiration - self._started
|
140
|
+
|
141
|
+
@duration.setter
|
142
|
+
def duration(self, value):
|
143
|
+
if self._started is None:
|
144
|
+
self._duration = value
|
145
|
+
else:
|
146
|
+
if self.ended:
|
147
|
+
raise interfaces.EndedError
|
148
|
+
old = self._expiration
|
149
|
+
if value is None:
|
150
|
+
self._expiration = value
|
151
|
+
elif not isinstance(value, datetime.timedelta):
|
152
|
+
raise ValueError('duration must be datetime.timedelta')
|
153
|
+
else:
|
154
|
+
if value < NO_DURATION:
|
155
|
+
raise ValueError('duration may not be negative')
|
156
|
+
self._expiration = self._started + value
|
157
|
+
if old != self._expiration:
|
158
|
+
self.utility.register(self)
|
159
|
+
event.notify(interfaces.ExpirationChangedEvent(self, old))
|
160
|
+
|
161
|
+
@property
|
162
|
+
def remaining_duration(self):
|
163
|
+
if self._started is None:
|
164
|
+
raise interfaces.UnregisteredError(self)
|
165
|
+
if self.ended is not None:
|
166
|
+
return NO_DURATION
|
167
|
+
if self._expiration is None:
|
168
|
+
return None
|
169
|
+
return self._expiration - utils.now()
|
170
|
+
|
171
|
+
@remaining_duration.setter
|
172
|
+
def remaining_duration(self, value):
|
173
|
+
if self._started is None:
|
174
|
+
raise interfaces.UnregisteredError(self)
|
175
|
+
if self.ended:
|
176
|
+
raise interfaces.EndedError
|
177
|
+
old = self._expiration
|
178
|
+
if value is None:
|
179
|
+
self._expiration = value
|
180
|
+
elif not isinstance(value, datetime.timedelta):
|
181
|
+
raise ValueError('duration must be datetime.timedelta')
|
182
|
+
else:
|
183
|
+
if value < NO_DURATION:
|
184
|
+
raise ValueError('duration may not be negative')
|
185
|
+
self._expiration = utils.now() + value
|
186
|
+
if old != self._expiration:
|
187
|
+
self.utility.register(self)
|
188
|
+
event.notify(interfaces.ExpirationChangedEvent(self, old))
|
189
|
+
|
190
|
+
_ended = None
|
191
|
+
|
192
|
+
@property
|
193
|
+
def ended(self):
|
194
|
+
if self._utility is None:
|
195
|
+
raise interfaces.UnregisteredError(self)
|
196
|
+
if self._ended is not None:
|
197
|
+
return self._ended
|
198
|
+
if (self._expiration is not None and
|
199
|
+
self._expiration <= utils.now()):
|
200
|
+
return self._expiration
|
201
|
+
|
202
|
+
def end(self):
|
203
|
+
if self.ended:
|
204
|
+
raise interfaces.EndedError
|
205
|
+
self._ended = utils.now()
|
206
|
+
self.utility.register(self)
|
207
|
+
event.notify(interfaces.TokenEndedEvent(self))
|
208
|
+
|
209
|
+
|
210
|
+
@interface.implementer(interfaces.IExclusiveLock)
|
211
|
+
class ExclusiveLock(EndableToken):
|
212
|
+
|
213
|
+
def __init__(self, target, principal_id, duration=None):
|
214
|
+
self._principal_ids = frozenset((principal_id,))
|
215
|
+
super().__init__(target, duration)
|
216
|
+
|
217
|
+
|
218
|
+
@interface.implementer(interfaces.ISharedLock)
|
219
|
+
class SharedLock(EndableToken):
|
220
|
+
|
221
|
+
def __init__(self, target, principal_ids, duration=None):
|
222
|
+
self._principal_ids = frozenset(principal_ids)
|
223
|
+
super().__init__(target, duration)
|
224
|
+
|
225
|
+
def add(self, principal_ids):
|
226
|
+
if self.ended:
|
227
|
+
raise interfaces.EndedError
|
228
|
+
old = self._principal_ids
|
229
|
+
self._principal_ids = self._principal_ids.union(principal_ids)
|
230
|
+
if old != self._principal_ids:
|
231
|
+
self.utility.register(self)
|
232
|
+
event.notify(interfaces.PrincipalsChangedEvent(self, old))
|
233
|
+
|
234
|
+
def remove(self, principal_ids):
|
235
|
+
if self.ended:
|
236
|
+
raise interfaces.EndedError
|
237
|
+
old = self._principal_ids
|
238
|
+
self._principal_ids = self._principal_ids.difference(principal_ids)
|
239
|
+
if not self._principal_ids:
|
240
|
+
self.end()
|
241
|
+
elif old != self._principal_ids:
|
242
|
+
self.utility.register(self)
|
243
|
+
else:
|
244
|
+
return
|
245
|
+
# principals changed if you got here
|
246
|
+
event.notify(interfaces.PrincipalsChangedEvent(self, old))
|
247
|
+
|
248
|
+
|
249
|
+
@interface.implementer(interfaces.IEndableFreeze)
|
250
|
+
class EndableFreeze(EndableToken):
|
251
|
+
pass
|
252
|
+
|
253
|
+
|
254
|
+
@interface.implementer(interfaces.IFreeze)
|
255
|
+
class Freeze(Token):
|
256
|
+
pass
|
zope/locking/utility.py
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
##############################################################################
|
2
|
+
#
|
3
|
+
# Copyright (c) 2018 Zope Foundation and Contributors.
|
4
|
+
# All Rights Reserved.
|
5
|
+
#
|
6
|
+
# This software is subject to the provisions of the Zope Public License,
|
7
|
+
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
8
|
+
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
9
|
+
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
10
|
+
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
11
|
+
# FOR A PARTICULAR PURPOSE.
|
12
|
+
#
|
13
|
+
##############################################################################
|
14
|
+
|
15
|
+
import persistent
|
16
|
+
import persistent.interfaces
|
17
|
+
from BTrees.OOBTree import OOBTree
|
18
|
+
from BTrees.OOBTree import OOTreeSet
|
19
|
+
from zope.keyreference.interfaces import IKeyReference
|
20
|
+
from zope.location import Location
|
21
|
+
|
22
|
+
from zope import event
|
23
|
+
from zope import interface
|
24
|
+
from zope.locking import interfaces
|
25
|
+
from zope.locking import utils
|
26
|
+
|
27
|
+
|
28
|
+
@interface.implementer(interfaces.ITokenUtility)
|
29
|
+
class TokenUtility(persistent.Persistent, Location):
|
30
|
+
|
31
|
+
def __init__(self):
|
32
|
+
self._locks = OOBTree()
|
33
|
+
self._expirations = OOBTree()
|
34
|
+
self._principal_ids = OOBTree()
|
35
|
+
|
36
|
+
def _del(self, tree, token, value):
|
37
|
+
"""remove a token for a value within either of the two index trees"""
|
38
|
+
reg = tree[value]
|
39
|
+
reg.remove(token)
|
40
|
+
if not reg:
|
41
|
+
del tree[value]
|
42
|
+
|
43
|
+
def _add(self, tree, token, value):
|
44
|
+
"""add a token for a value within either of the two index trees"""
|
45
|
+
reg = tree.get(value)
|
46
|
+
if reg is None:
|
47
|
+
reg = tree[value] = OOTreeSet()
|
48
|
+
reg.insert(token)
|
49
|
+
|
50
|
+
def _cleanup(self):
|
51
|
+
"clean out expired keys"
|
52
|
+
expiredkeys = []
|
53
|
+
for k in self._expirations.keys(max=utils.now()):
|
54
|
+
for token in self._expirations[k]:
|
55
|
+
assert token.ended
|
56
|
+
for p in token.principal_ids:
|
57
|
+
self._del(self._principal_ids, token, p)
|
58
|
+
key_ref = IKeyReference(token.context)
|
59
|
+
del self._locks[key_ref]
|
60
|
+
expiredkeys.append(k)
|
61
|
+
for k in expiredkeys:
|
62
|
+
del self._expirations[k]
|
63
|
+
|
64
|
+
def register(self, token):
|
65
|
+
assert interfaces.IToken.providedBy(token)
|
66
|
+
if token.utility is None:
|
67
|
+
token.utility = self
|
68
|
+
elif token.utility is not self:
|
69
|
+
raise ValueError('Lock is already registered with another utility')
|
70
|
+
if persistent.interfaces.IPersistent.providedBy(token):
|
71
|
+
self._p_jar.add(token)
|
72
|
+
key_ref = IKeyReference(token.context)
|
73
|
+
current = self._locks.get(key_ref)
|
74
|
+
if current is not None:
|
75
|
+
current, principal_ids, expiration = current
|
76
|
+
current_endable = interfaces.IEndable.providedBy(current)
|
77
|
+
if current is not token:
|
78
|
+
if current_endable and not current.ended:
|
79
|
+
raise interfaces.RegistrationError(token)
|
80
|
+
# expired token: clean up indexes and fall through
|
81
|
+
if current_endable and expiration is not None:
|
82
|
+
self._del(self._expirations, current, expiration)
|
83
|
+
for p in principal_ids:
|
84
|
+
self._del(self._principal_ids, current, p)
|
85
|
+
else:
|
86
|
+
# current is token; reindex and return
|
87
|
+
if current_endable and token.ended:
|
88
|
+
if expiration is not None:
|
89
|
+
self._del(self._expirations, token, expiration)
|
90
|
+
for p in principal_ids:
|
91
|
+
self._del(self._principal_ids, token, p)
|
92
|
+
del self._locks[key_ref]
|
93
|
+
else:
|
94
|
+
if current_endable and token.expiration != expiration:
|
95
|
+
# reindex timeout
|
96
|
+
if expiration is not None:
|
97
|
+
self._del(self._expirations, token, expiration)
|
98
|
+
if token.expiration is not None:
|
99
|
+
self._add(
|
100
|
+
self._expirations, token, token.expiration)
|
101
|
+
orig = frozenset(principal_ids)
|
102
|
+
new = frozenset(token.principal_ids)
|
103
|
+
removed = orig.difference(new)
|
104
|
+
added = new.difference(orig)
|
105
|
+
for p in removed:
|
106
|
+
self._del(self._principal_ids, token, p)
|
107
|
+
for p in added:
|
108
|
+
self._add(self._principal_ids, token, p)
|
109
|
+
self._locks[key_ref] = (
|
110
|
+
token,
|
111
|
+
frozenset(token.principal_ids),
|
112
|
+
current_endable and token.expiration or None)
|
113
|
+
self._cleanup()
|
114
|
+
return token
|
115
|
+
# expired current token or no current token; this is new
|
116
|
+
endable = interfaces.IEndable.providedBy(token)
|
117
|
+
self._locks[key_ref] = (
|
118
|
+
token,
|
119
|
+
frozenset(token.principal_ids),
|
120
|
+
endable and token.expiration or None)
|
121
|
+
if (endable and
|
122
|
+
token.expiration is not None):
|
123
|
+
self._add(self._expirations, token, token.expiration)
|
124
|
+
for p in token.principal_ids:
|
125
|
+
self._add(self._principal_ids, token, p)
|
126
|
+
self._cleanup()
|
127
|
+
event.notify(interfaces.TokenStartedEvent(token))
|
128
|
+
return token
|
129
|
+
|
130
|
+
def get(self, obj, default=None):
|
131
|
+
res = self._locks.get(IKeyReference(obj))
|
132
|
+
if res is not None and (
|
133
|
+
not interfaces.IEndable.providedBy(res[0])
|
134
|
+
or not res[0].ended):
|
135
|
+
return res[0]
|
136
|
+
return default
|
137
|
+
|
138
|
+
def iterForPrincipalId(self, principal_id):
|
139
|
+
locks = self._principal_ids.get(principal_id, ())
|
140
|
+
for lock in locks:
|
141
|
+
assert principal_id in frozenset(lock.principal_ids)
|
142
|
+
if not lock.ended:
|
143
|
+
yield lock
|
144
|
+
|
145
|
+
def __iter__(self):
|
146
|
+
for lock in self._locks.values():
|
147
|
+
if (not interfaces.IEndable.providedBy(lock[0])
|
148
|
+
or not lock[0].ended):
|
149
|
+
yield lock[0]
|
zope/locking/utils.py
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
##############################################################################
|
2
|
+
#
|
3
|
+
# Copyright (c) 2018 Zope Foundation and Contributors.
|
4
|
+
# All Rights Reserved.
|
5
|
+
#
|
6
|
+
# This software is subject to the provisions of the Zope Public License,
|
7
|
+
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
8
|
+
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
9
|
+
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
10
|
+
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
11
|
+
# FOR A PARTICULAR PURPOSE.
|
12
|
+
#
|
13
|
+
##############################################################################
|
14
|
+
|
15
|
+
import datetime
|
16
|
+
|
17
|
+
import pytz
|
18
|
+
|
19
|
+
|
20
|
+
# this is a small convenience, but is more important as a convenient monkey-
|
21
|
+
# patch opportunity for the package's README.txt doctest.
|
22
|
+
def now():
|
23
|
+
return datetime.datetime.now(pytz.utc)
|
@@ -0,0 +1 @@
|
|
1
|
+
import sys, types, os;p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('zope',));importlib = __import__('importlib.util');__import__('importlib.machinery');m = sys.modules.setdefault('zope', importlib.util.module_from_spec(importlib.machinery.PathFinder.find_spec('zope', [os.path.dirname(p)])));m = m or sys.modules.setdefault('zope', types.ModuleType('zope'));mp = (m or []) and m.__dict__.setdefault('__path__',[]);(p not in mp) and mp.append(p)
|
@@ -0,0 +1,44 @@
|
|
1
|
+
Zope Public License (ZPL) Version 2.1
|
2
|
+
|
3
|
+
A copyright notice accompanies this license document that identifies the
|
4
|
+
copyright holders.
|
5
|
+
|
6
|
+
This license has been certified as open source. It has also been designated as
|
7
|
+
GPL compatible by the Free Software Foundation (FSF).
|
8
|
+
|
9
|
+
Redistribution and use in source and binary forms, with or without
|
10
|
+
modification, are permitted provided that the following conditions are met:
|
11
|
+
|
12
|
+
1. Redistributions in source code must retain the accompanying copyright
|
13
|
+
notice, this list of conditions, and the following disclaimer.
|
14
|
+
|
15
|
+
2. Redistributions in binary form must reproduce the accompanying copyright
|
16
|
+
notice, this list of conditions, and the following disclaimer in the
|
17
|
+
documentation and/or other materials provided with the distribution.
|
18
|
+
|
19
|
+
3. Names of the copyright holders must not be used to endorse or promote
|
20
|
+
products derived from this software without prior written permission from the
|
21
|
+
copyright holders.
|
22
|
+
|
23
|
+
4. The right to distribute this software or to use it for any purpose does not
|
24
|
+
give you the right to use Servicemarks (sm) or Trademarks (tm) of the
|
25
|
+
copyright
|
26
|
+
holders. Use of them is covered by separate agreement with the copyright
|
27
|
+
holders.
|
28
|
+
|
29
|
+
5. If any files are modified, you must cause the modified files to carry
|
30
|
+
prominent notices stating that you changed the files and the date of any
|
31
|
+
change.
|
32
|
+
|
33
|
+
Disclaimer
|
34
|
+
|
35
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED
|
36
|
+
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
37
|
+
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
38
|
+
EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
39
|
+
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
40
|
+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
41
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
42
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
43
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
44
|
+
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|