Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/psycopg2/_range.py: 30%
264 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1"""Implementation of the Range type and adaptation
3"""
5# psycopg/_range.py - Implementation of the Range type and adaptation
6#
7# Copyright (C) 2012-2019 Daniele Varrazzo <daniele.varrazzo@gmail.com>
8# Copyright (C) 2020-2021 The Psycopg Team
9#
10# psycopg2 is free software: you can redistribute it and/or modify it
11# under the terms of the GNU Lesser General Public License as published
12# by the Free Software Foundation, either version 3 of the License, or
13# (at your option) any later version.
14#
15# In addition, as a special exception, the copyright holders give
16# permission to link this program with the OpenSSL library (or with
17# modified versions of OpenSSL that use the same license as OpenSSL),
18# and distribute linked combinations including the two.
19#
20# You must obey the GNU Lesser General Public License in all respects for
21# all of the code used other than OpenSSL.
22#
23# psycopg2 is distributed in the hope that it will be useful, but WITHOUT
24# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
25# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
26# License for more details.
28import re
30from psycopg2._psycopg import ProgrammingError, InterfaceError
31from psycopg2.extensions import ISQLQuote, adapt, register_adapter
32from psycopg2.extensions import new_type, new_array_type, register_type
35class Range:
36 """Python representation for a PostgreSQL |range|_ type.
38 :param lower: lower bound for the range. `!None` means unbound
39 :param upper: upper bound for the range. `!None` means unbound
40 :param bounds: one of the literal strings ``()``, ``[)``, ``(]``, ``[]``,
41 representing whether the lower or upper bounds are included
42 :param empty: if `!True`, the range is empty
44 """
45 __slots__ = ('_lower', '_upper', '_bounds')
47 def __init__(self, lower=None, upper=None, bounds='[)', empty=False):
48 if not empty:
49 if bounds not in ('[)', '(]', '()', '[]'):
50 raise ValueError(f"bound flags not valid: {bounds!r}")
52 self._lower = lower
53 self._upper = upper
54 self._bounds = bounds
55 else:
56 self._lower = self._upper = self._bounds = None
58 def __repr__(self):
59 if self._bounds is None:
60 return f"{self.__class__.__name__}(empty=True)"
61 else:
62 return "{}({!r}, {!r}, {!r})".format(self.__class__.__name__,
63 self._lower, self._upper, self._bounds)
65 def __str__(self):
66 if self._bounds is None:
67 return 'empty'
69 items = [
70 self._bounds[0],
71 str(self._lower),
72 ', ',
73 str(self._upper),
74 self._bounds[1]
75 ]
76 return ''.join(items)
78 @property
79 def lower(self):
80 """The lower bound of the range. `!None` if empty or unbound."""
81 return self._lower
83 @property
84 def upper(self):
85 """The upper bound of the range. `!None` if empty or unbound."""
86 return self._upper
88 @property
89 def isempty(self):
90 """`!True` if the range is empty."""
91 return self._bounds is None
93 @property
94 def lower_inf(self):
95 """`!True` if the range doesn't have a lower bound."""
96 if self._bounds is None:
97 return False
98 return self._lower is None
100 @property
101 def upper_inf(self):
102 """`!True` if the range doesn't have an upper bound."""
103 if self._bounds is None:
104 return False
105 return self._upper is None
107 @property
108 def lower_inc(self):
109 """`!True` if the lower bound is included in the range."""
110 if self._bounds is None or self._lower is None:
111 return False
112 return self._bounds[0] == '['
114 @property
115 def upper_inc(self):
116 """`!True` if the upper bound is included in the range."""
117 if self._bounds is None or self._upper is None:
118 return False
119 return self._bounds[1] == ']'
121 def __contains__(self, x):
122 if self._bounds is None:
123 return False
125 if self._lower is not None:
126 if self._bounds[0] == '[':
127 if x < self._lower:
128 return False
129 else:
130 if x <= self._lower:
131 return False
133 if self._upper is not None:
134 if self._bounds[1] == ']':
135 if x > self._upper:
136 return False
137 else:
138 if x >= self._upper:
139 return False
141 return True
143 def __bool__(self):
144 return self._bounds is not None
146 def __eq__(self, other):
147 if not isinstance(other, Range):
148 return False
149 return (self._lower == other._lower
150 and self._upper == other._upper
151 and self._bounds == other._bounds)
153 def __ne__(self, other):
154 return not self.__eq__(other)
156 def __hash__(self):
157 return hash((self._lower, self._upper, self._bounds))
159 # as the postgres docs describe for the server-side stuff,
160 # ordering is rather arbitrary, but will remain stable
161 # and consistent.
163 def __lt__(self, other):
164 if not isinstance(other, Range):
165 return NotImplemented
166 for attr in ('_lower', '_upper', '_bounds'):
167 self_value = getattr(self, attr)
168 other_value = getattr(other, attr)
169 if self_value == other_value:
170 pass
171 elif self_value is None:
172 return True
173 elif other_value is None:
174 return False
175 else:
176 return self_value < other_value
177 return False
179 def __le__(self, other):
180 if self == other:
181 return True
182 else:
183 return self.__lt__(other)
185 def __gt__(self, other):
186 if isinstance(other, Range):
187 return other.__lt__(self)
188 else:
189 return NotImplemented
191 def __ge__(self, other):
192 if self == other:
193 return True
194 else:
195 return self.__gt__(other)
197 def __getstate__(self):
198 return {slot: getattr(self, slot)
199 for slot in self.__slots__ if hasattr(self, slot)}
201 def __setstate__(self, state):
202 for slot, value in state.items():
203 setattr(self, slot, value)
206def register_range(pgrange, pyrange, conn_or_curs, globally=False):
207 """Create and register an adapter and the typecasters to convert between
208 a PostgreSQL |range|_ type and a PostgreSQL `Range` subclass.
210 :param pgrange: the name of the PostgreSQL |range| type. Can be
211 schema-qualified
212 :param pyrange: a `Range` strict subclass, or just a name to give to a new
213 class
214 :param conn_or_curs: a connection or cursor used to find the oid of the
215 range and its subtype; the typecaster is registered in a scope limited
216 to this object, unless *globally* is set to `!True`
217 :param globally: if `!False` (default) register the typecaster only on
218 *conn_or_curs*, otherwise register it globally
219 :return: `RangeCaster` instance responsible for the conversion
221 If a string is passed to *pyrange*, a new `Range` subclass is created
222 with such name and will be available as the `~RangeCaster.range` attribute
223 of the returned `RangeCaster` object.
225 The function queries the database on *conn_or_curs* to inspect the
226 *pgrange* type and raises `~psycopg2.ProgrammingError` if the type is not
227 found. If querying the database is not advisable, use directly the
228 `RangeCaster` class and register the adapter and typecasters using the
229 provided functions.
231 """
232 caster = RangeCaster._from_db(pgrange, pyrange, conn_or_curs)
233 caster._register(not globally and conn_or_curs or None)
234 return caster
237class RangeAdapter:
238 """`ISQLQuote` adapter for `Range` subclasses.
240 This is an abstract class: concrete classes must set a `name` class
241 attribute or override `getquoted()`.
242 """
243 name = None
245 def __init__(self, adapted):
246 self.adapted = adapted
248 def __conform__(self, proto):
249 if self._proto is ISQLQuote:
250 return self
252 def prepare(self, conn):
253 self._conn = conn
255 def getquoted(self):
256 if self.name is None:
257 raise NotImplementedError(
258 'RangeAdapter must be subclassed overriding its name '
259 'or the getquoted() method')
261 r = self.adapted
262 if r.isempty:
263 return b"'empty'::" + self.name.encode('utf8')
265 if r.lower is not None:
266 a = adapt(r.lower)
267 if hasattr(a, 'prepare'):
268 a.prepare(self._conn)
269 lower = a.getquoted()
270 else:
271 lower = b'NULL'
273 if r.upper is not None:
274 a = adapt(r.upper)
275 if hasattr(a, 'prepare'):
276 a.prepare(self._conn)
277 upper = a.getquoted()
278 else:
279 upper = b'NULL'
281 return self.name.encode('utf8') + b'(' + lower + b', ' + upper \
282 + b", '" + r._bounds.encode('utf8') + b"')"
285class RangeCaster:
286 """Helper class to convert between `Range` and PostgreSQL range types.
288 Objects of this class are usually created by `register_range()`. Manual
289 creation could be useful if querying the database is not advisable: in
290 this case the oids must be provided.
291 """
292 def __init__(self, pgrange, pyrange, oid, subtype_oid, array_oid=None):
293 self.subtype_oid = subtype_oid
294 self._create_ranges(pgrange, pyrange)
296 name = self.adapter.name or self.adapter.__class__.__name__
298 self.typecaster = new_type((oid,), name, self.parse)
300 if array_oid is not None: 300 ↛ 304line 300 didn't jump to line 304, because the condition on line 300 was never false
301 self.array_typecaster = new_array_type(
302 (array_oid,), name + "ARRAY", self.typecaster)
303 else:
304 self.array_typecaster = None
306 def _create_ranges(self, pgrange, pyrange):
307 """Create Range and RangeAdapter classes if needed."""
308 # if got a string create a new RangeAdapter concrete type (with a name)
309 # else take it as an adapter. Passing an adapter should be considered
310 # an implementation detail and is not documented. It is currently used
311 # for the numeric ranges.
312 self.adapter = None
313 if isinstance(pgrange, str):
314 self.adapter = type(pgrange, (RangeAdapter,), {})
315 self.adapter.name = pgrange
316 else:
317 try:
318 if issubclass(pgrange, RangeAdapter) \ 318 ↛ 324line 318 didn't jump to line 324, because the condition on line 318 was never false
319 and pgrange is not RangeAdapter:
320 self.adapter = pgrange
321 except TypeError:
322 pass
324 if self.adapter is None: 324 ↛ 325line 324 didn't jump to line 325, because the condition on line 324 was never true
325 raise TypeError(
326 'pgrange must be a string or a RangeAdapter strict subclass')
328 self.range = None
329 try:
330 if isinstance(pyrange, str): 330 ↛ 331line 330 didn't jump to line 331, because the condition on line 330 was never true
331 self.range = type(pyrange, (Range,), {})
332 if issubclass(pyrange, Range) and pyrange is not Range: 332 ↛ 337line 332 didn't jump to line 337, because the condition on line 332 was never false
333 self.range = pyrange
334 except TypeError:
335 pass
337 if self.range is None: 337 ↛ 338line 337 didn't jump to line 338, because the condition on line 337 was never true
338 raise TypeError(
339 'pyrange must be a type or a Range strict subclass')
341 @classmethod
342 def _from_db(self, name, pyrange, conn_or_curs):
343 """Return a `RangeCaster` instance for the type *pgrange*.
345 Raise `ProgrammingError` if the type is not found.
346 """
347 from psycopg2.extensions import STATUS_IN_TRANSACTION
348 from psycopg2.extras import _solve_conn_curs
349 conn, curs = _solve_conn_curs(conn_or_curs)
351 if conn.info.server_version < 90200:
352 raise ProgrammingError("range types not available in version %s"
353 % conn.info.server_version)
355 # Store the transaction status of the connection to revert it after use
356 conn_status = conn.status
358 # Use the correct schema
359 if '.' in name:
360 schema, tname = name.split('.', 1)
361 else:
362 tname = name
363 schema = 'public'
365 # get the type oid and attributes
366 curs.execute("""\
367select rngtypid, rngsubtype, typarray
368from pg_range r
369join pg_type t on t.oid = rngtypid
370join pg_namespace ns on ns.oid = typnamespace
371where typname = %s and ns.nspname = %s;
372""", (tname, schema))
373 rec = curs.fetchone()
375 if not rec:
376 # The above algorithm doesn't work for customized seach_path
377 # (#1487) The implementation below works better, but, to guarantee
378 # backwards compatibility, use it only if the original one failed.
379 try:
380 savepoint = False
381 # Because we executed statements earlier, we are either INTRANS
382 # or we are IDLE only if the transaction is autocommit, in
383 # which case we don't need the savepoint anyway.
384 if conn.status == STATUS_IN_TRANSACTION:
385 curs.execute("SAVEPOINT register_type")
386 savepoint = True
388 curs.execute("""\
389SELECT rngtypid, rngsubtype, typarray, typname, nspname
390from pg_range r
391join pg_type t on t.oid = rngtypid
392join pg_namespace ns on ns.oid = typnamespace
393WHERE t.oid = %s::regtype
394""", (name, ))
395 except ProgrammingError:
396 pass
397 else:
398 rec = curs.fetchone()
399 if rec:
400 tname, schema = rec[3:]
401 finally:
402 if savepoint:
403 curs.execute("ROLLBACK TO SAVEPOINT register_type")
405 # revert the status of the connection as before the command
406 if conn_status != STATUS_IN_TRANSACTION and not conn.autocommit:
407 conn.rollback()
409 if not rec:
410 raise ProgrammingError(
411 f"PostgreSQL range '{name}' not found")
413 type, subtype, array = rec[:3]
415 return RangeCaster(name, pyrange,
416 oid=type, subtype_oid=subtype, array_oid=array)
418 _re_range = re.compile(r"""
419 ( \(|\[ ) # lower bound flag
420 (?: # lower bound:
421 " ( (?: [^"] | "")* ) " # - a quoted string
422 | ( [^",]+ ) # - or an unquoted string
423 )? # - or empty (not catched)
424 ,
425 (?: # upper bound:
426 " ( (?: [^"] | "")* ) " # - a quoted string
427 | ( [^"\)\]]+ ) # - or an unquoted string
428 )? # - or empty (not catched)
429 ( \)|\] ) # upper bound flag
430 """, re.VERBOSE)
432 _re_undouble = re.compile(r'(["\\])\1')
434 def parse(self, s, cur=None):
435 if s is None:
436 return None
438 if s == 'empty':
439 return self.range(empty=True)
441 m = self._re_range.match(s)
442 if m is None:
443 raise InterfaceError(f"failed to parse range: '{s}'")
445 lower = m.group(3)
446 if lower is None:
447 lower = m.group(2)
448 if lower is not None:
449 lower = self._re_undouble.sub(r"\1", lower)
451 upper = m.group(5)
452 if upper is None:
453 upper = m.group(4)
454 if upper is not None:
455 upper = self._re_undouble.sub(r"\1", upper)
457 if cur is not None:
458 lower = cur.cast(self.subtype_oid, lower)
459 upper = cur.cast(self.subtype_oid, upper)
461 bounds = m.group(1) + m.group(6)
463 return self.range(lower, upper, bounds)
465 def _register(self, scope=None):
466 register_type(self.typecaster, scope)
467 if self.array_typecaster is not None: 467 ↛ 470line 467 didn't jump to line 470, because the condition on line 467 was never false
468 register_type(self.array_typecaster, scope)
470 register_adapter(self.range, self.adapter)
473class NumericRange(Range):
474 """A `Range` suitable to pass Python numeric types to a PostgreSQL range.
476 PostgreSQL types :sql:`int4range`, :sql:`int8range`, :sql:`numrange` are
477 casted into `!NumericRange` instances.
478 """
479 pass
482class DateRange(Range):
483 """Represents :sql:`daterange` values."""
484 pass
487class DateTimeRange(Range):
488 """Represents :sql:`tsrange` values."""
489 pass
492class DateTimeTZRange(Range):
493 """Represents :sql:`tstzrange` values."""
494 pass
497# Special adaptation for NumericRange. Allows to pass number range regardless
498# of whether they are ints, floats and what size of ints are, which are
499# pointless in Python world. On the way back, no numeric range is casted to
500# NumericRange, but only to their subclasses
502class NumberRangeAdapter(RangeAdapter):
503 """Adapt a range if the subtype doesn't need quotes."""
504 def getquoted(self):
505 r = self.adapted
506 if r.isempty:
507 return b"'empty'"
509 if not r.lower_inf:
510 # not exactly: we are relying that none of these object is really
511 # quoted (they are numbers). Also, I'm lazy and not preparing the
512 # adapter because I assume encoding doesn't matter for these
513 # objects.
514 lower = adapt(r.lower).getquoted().decode('ascii')
515 else:
516 lower = ''
518 if not r.upper_inf:
519 upper = adapt(r.upper).getquoted().decode('ascii')
520 else:
521 upper = ''
523 return (f"'{r._bounds[0]}{lower},{upper}{r._bounds[1]}'").encode('ascii')
526# TODO: probably won't work with infs, nans and other tricky cases.
527register_adapter(NumericRange, NumberRangeAdapter)
529# Register globally typecasters and adapters for builtin range types.
531# note: the adapter is registered more than once, but this is harmless.
532int4range_caster = RangeCaster(NumberRangeAdapter, NumericRange,
533 oid=3904, subtype_oid=23, array_oid=3905)
534int4range_caster._register()
536int8range_caster = RangeCaster(NumberRangeAdapter, NumericRange,
537 oid=3926, subtype_oid=20, array_oid=3927)
538int8range_caster._register()
540numrange_caster = RangeCaster(NumberRangeAdapter, NumericRange,
541 oid=3906, subtype_oid=1700, array_oid=3907)
542numrange_caster._register()
544daterange_caster = RangeCaster('daterange', DateRange,
545 oid=3912, subtype_oid=1082, array_oid=3913)
546daterange_caster._register()
548tsrange_caster = RangeCaster('tsrange', DateTimeRange,
549 oid=3908, subtype_oid=1114, array_oid=3909)
550tsrange_caster._register()
552tstzrange_caster = RangeCaster('tstzrange', DateTimeTZRange,
553 oid=3910, subtype_oid=1184, array_oid=3911)
554tstzrange_caster._register()