Skip to content

Commit cd712ab

Browse files
committed
Draft new response handling API
1 parent e75c24d commit cd712ab

File tree

6 files changed

+819
-11
lines changed

6 files changed

+819
-11
lines changed

Lib/ldap/connection.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""
2+
connection.py - wraps class _ldap.LDAPObject
3+
4+
See https://www.python-ldap.org/ for details.
5+
"""
6+
7+
from ldap.pkginfo import __version__, __author__, __license__
8+
9+
__all__ = [
10+
'Connection',
11+
]
12+
13+
14+
if __debug__:
15+
# Tracing is only supported in debugging mode
16+
import traceback
17+
18+
import sys,time,pprint,_ldap,ldap,ldap.sasl,ldap.functions
19+
import warnings
20+
21+
from ldap import LDAPError
22+
from ldap.controls import DecodeControlTuples
23+
from ldap.ldapobject import SimpleLDAPObject
24+
from ldap.response import Response
25+
26+
27+
class Connection(SimpleLDAPObject):
28+
resp_ctrl_classes = None
29+
30+
def result(self, msgid=ldap.RES_ANY, all=1, timeout=None):
31+
"""
32+
result([msgid=RES_ANY [,all=1 [,timeout=None]]]) -> Optional[list[LDAPMessage]]
33+
34+
This method is used to wait for and return the result of an
35+
operation previously initiated by one of the LDAP asynchronous
36+
operation routines (e.g. search(), modify(), etc.) They all
37+
return an invocation identifier (a message id) upon successful
38+
initiation of their operation. This id is guaranteed to be
39+
unique across an LDAP session, and can be used to request the
40+
result of a specific operation via the msgid parameter of the
41+
result() method.
42+
43+
If the result of a specific operation is required, msgid should
44+
be set to the invocation message id returned when the operation
45+
was initiated; otherwise RES_ANY should be supplied.
46+
47+
The all parameter is used to wait until a final response for
48+
a given operation is received, this is useful with operations
49+
(like search) that generate multiple responses and is used
50+
to select whether a single item should be returned or to wait
51+
for all the responses before returning.
52+
53+
Using search as an example: A search response is made up of
54+
zero or more search entries followed by a search result. If all
55+
is 0, search entries will be returned one at a time as they
56+
come in, via separate calls to result(). If all is 1, the
57+
search response will be returned in its entirety, i.e. after
58+
all entries and the final search result have been received. If
59+
all is 2, all search entries that have been received so far
60+
will be returned.
61+
62+
The method returns a list of messages or None if polling and no
63+
messages arrived yet.
64+
65+
The result() method will block for timeout seconds, or
66+
indefinitely if timeout is negative. A timeout of 0 will
67+
effect a poll. The timeout can be expressed as a floating-point
68+
value. If timeout is None the default in self.timeout is used.
69+
70+
If a timeout occurs, a TIMEOUT exception is raised, unless
71+
polling (timeout = 0), in which case None is returned.
72+
"""
73+
74+
if timeout is None:
75+
timeout = self.timeout
76+
77+
messages = self._ldap_call(self._l.result, msgid, all, timeout)
78+
79+
if messages is None:
80+
return None
81+
82+
results = []
83+
for msgid, msgtype, controls, data in messages:
84+
controls = DecodeControlTuples(controls, self.resp_ctrl_classes)
85+
86+
m = Response(msgid, msgtype, controls, **data)
87+
results.append(m)
88+
89+
return results

Lib/ldap/response.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
"""
2+
connection.py - wraps class _ldap.LDAPObject
3+
4+
See https://www.python-ldap.org/ for details.
5+
"""
6+
7+
from ldap.pkginfo import __version__, __author__, __license__
8+
9+
__all__ = [
10+
'Response',
11+
'Result',
12+
13+
'SearchEntry'
14+
'SearchReference',
15+
'SearchResult',
16+
17+
'IntermediateResponse',
18+
'ExtendedResult',
19+
20+
'BindResult',
21+
'ModifyResult',
22+
'AddResult',
23+
'DeleteResult',
24+
'ModRDNResult',
25+
'CompareResult',
26+
]
27+
28+
29+
import ldap
30+
from typing import Optional
31+
32+
33+
from ldap import LDAPError
34+
from ldap.controls import LDAPControl
35+
36+
37+
_SUCCESS_CODES = [
38+
ldap.SUCCESS,
39+
ldap.COMPARE_TRUE,
40+
ldap.COMPARE_FALSE,
41+
ldap.SASL_BIND_IN_PROGRESS,
42+
]
43+
44+
45+
class Response:
46+
msgid: int
47+
msgtype: int
48+
controls: list[LDAPControl]
49+
50+
__subclasses: dict[int, type] = {}
51+
52+
def __init_subclass__(cls):
53+
if not hasattr(cls, 'msgtype'):
54+
return
55+
c = __class__.__subclasses.setdefault(cls.msgtype, cls)
56+
assert issubclass(cls, c)
57+
58+
def __new__(cls, msgid, msgtype, controls=None, **kwargs):
59+
if cls is not __class__:
60+
return super().__new__(cls)
61+
62+
c = __class__.__subclasses.get(msgtype)
63+
if c:
64+
return c.__new__(c, msgid, msgtype, controls, **kwargs)
65+
66+
instance = super().__new__(cls)
67+
instance.msgid = msgid
68+
instance.msgtype = msgtype
69+
instance.controls = controls
70+
return instance
71+
72+
73+
class Result(Response):
74+
result: int
75+
matcheddn: str
76+
message: str
77+
referrals: Optional[list[str]]
78+
79+
def __new__(cls, msgid, msgtype, controls,
80+
result, matcheddn, message, referrals, **kwargs):
81+
instance = super().__new__(cls, msgid, msgtype, controls, **kwargs)
82+
83+
instance.result = result
84+
instance.matcheddn = matcheddn
85+
instance.message = message
86+
instance.referrals = referrals
87+
88+
return instance
89+
90+
def raise_for_result(self):
91+
if self.result in _SUCCESS_CODES:
92+
return
93+
raise LDAPError(self)
94+
95+
96+
class SearchEntry(Response):
97+
msgtype = ldap.RES_SEARCH_ENTRY
98+
99+
dn: str
100+
attrs: dict[str, Optional[list[bytes]]]
101+
102+
def __new__(cls, msgid, msgtype, controls, dn, attrs, **kwargs):
103+
instance = super().__new__(cls, msgid, msgtype, controls, **kwargs)
104+
105+
instance.dn = dn
106+
instance.attrs = attrs
107+
108+
return instance
109+
110+
111+
class SearchReference(Response):
112+
msgtype = ldap.RES_SEARCH_REFERENCE
113+
114+
referrals: list[str]
115+
116+
def __new__(cls, msgid, msgtype, controls, referrals, **kwargs):
117+
instance = super().__new__(cls, msgid, msgtype, controls, **kwargs)
118+
119+
instance.referrals = referrals
120+
121+
return instance
122+
123+
124+
class SearchResult(Result):
125+
msgtype = ldap.RES_SEARCH_RESULT
126+
127+
128+
class IntermediateResponse(Response):
129+
msgtype = ldap.RES_INTERMEDIATE
130+
131+
oid: Optional[str]
132+
value: Optional[bytes]
133+
134+
__subclasses: dict[str, type] = {}
135+
136+
def __new__(cls, msgid, msgtype, controls=None, **kwargs):
137+
# TODO: instantiate
138+
pass
139+
140+
141+
class BindResult(Result):
142+
msgtype = ldap.RES_BIND
143+
144+
servercreds: Optional[bytes]
145+
146+
147+
class ModifyResult(Result):
148+
msgtype = ldap.RES_MODIFY
149+
150+
151+
class AddResult(Result):
152+
msgtype = ldap.RES_ADD
153+
154+
155+
class DeleteResult(Result):
156+
msgtype = ldap.RES_DELETE
157+
158+
159+
class ModRDNResult(Result):
160+
msgtype = ldap.RES_MODRDN
161+
162+
163+
class CompareResult(Result):
164+
msgtype = ldap.RES_COMPARE
165+
166+
def __bool__(self):
167+
if self.result == ldap.COMPARE_FALSE:
168+
return False
169+
if self.result == ldap.COMPARE_TRUE:
170+
return True
171+
raise LDAPError(self)
172+
173+
174+
class ExtendedResult(Result):
175+
msgtype = ldap.RES_EXTENDED
176+
177+
oid: Optional[str]
178+
value: Optional[bytes]
179+
# TODO: how to subclass these dynamically? (UnsolicitedResponse, ...),
180+
# is it just with __new__?

0 commit comments

Comments
 (0)