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

1"""Implementation of the Range type and adaptation 

2 

3""" 

4 

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. 

27 

28import re 

29 

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 

33 

34 

35class Range: 

36 """Python representation for a PostgreSQL |range|_ type. 

37 

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 

43 

44 """ 

45 __slots__ = ('_lower', '_upper', '_bounds') 

46 

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}") 

51 

52 self._lower = lower 

53 self._upper = upper 

54 self._bounds = bounds 

55 else: 

56 self._lower = self._upper = self._bounds = None 

57 

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) 

64 

65 def __str__(self): 

66 if self._bounds is None: 

67 return 'empty' 

68 

69 items = [ 

70 self._bounds[0], 

71 str(self._lower), 

72 ', ', 

73 str(self._upper), 

74 self._bounds[1] 

75 ] 

76 return ''.join(items) 

77 

78 @property 

79 def lower(self): 

80 """The lower bound of the range. `!None` if empty or unbound.""" 

81 return self._lower 

82 

83 @property 

84 def upper(self): 

85 """The upper bound of the range. `!None` if empty or unbound.""" 

86 return self._upper 

87 

88 @property 

89 def isempty(self): 

90 """`!True` if the range is empty.""" 

91 return self._bounds is None 

92 

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 

99 

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 

106 

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] == '[' 

113 

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] == ']' 

120 

121 def __contains__(self, x): 

122 if self._bounds is None: 

123 return False 

124 

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 

132 

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 

140 

141 return True 

142 

143 def __bool__(self): 

144 return self._bounds is not None 

145 

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) 

152 

153 def __ne__(self, other): 

154 return not self.__eq__(other) 

155 

156 def __hash__(self): 

157 return hash((self._lower, self._upper, self._bounds)) 

158 

159 # as the postgres docs describe for the server-side stuff, 

160 # ordering is rather arbitrary, but will remain stable 

161 # and consistent. 

162 

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 

178 

179 def __le__(self, other): 

180 if self == other: 

181 return True 

182 else: 

183 return self.__lt__(other) 

184 

185 def __gt__(self, other): 

186 if isinstance(other, Range): 

187 return other.__lt__(self) 

188 else: 

189 return NotImplemented 

190 

191 def __ge__(self, other): 

192 if self == other: 

193 return True 

194 else: 

195 return self.__gt__(other) 

196 

197 def __getstate__(self): 

198 return {slot: getattr(self, slot) 

199 for slot in self.__slots__ if hasattr(self, slot)} 

200 

201 def __setstate__(self, state): 

202 for slot, value in state.items(): 

203 setattr(self, slot, value) 

204 

205 

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. 

209 

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 

220 

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. 

224 

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. 

230 

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 

235 

236 

237class RangeAdapter: 

238 """`ISQLQuote` adapter for `Range` subclasses. 

239 

240 This is an abstract class: concrete classes must set a `name` class 

241 attribute or override `getquoted()`. 

242 """ 

243 name = None 

244 

245 def __init__(self, adapted): 

246 self.adapted = adapted 

247 

248 def __conform__(self, proto): 

249 if self._proto is ISQLQuote: 

250 return self 

251 

252 def prepare(self, conn): 

253 self._conn = conn 

254 

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') 

260 

261 r = self.adapted 

262 if r.isempty: 

263 return b"'empty'::" + self.name.encode('utf8') 

264 

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' 

272 

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' 

280 

281 return self.name.encode('utf8') + b'(' + lower + b', ' + upper \ 

282 + b", '" + r._bounds.encode('utf8') + b"')" 

283 

284 

285class RangeCaster: 

286 """Helper class to convert between `Range` and PostgreSQL range types. 

287 

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) 

295 

296 name = self.adapter.name or self.adapter.__class__.__name__ 

297 

298 self.typecaster = new_type((oid,), name, self.parse) 

299 

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 

305 

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 

323 

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') 

327 

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 

336 

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') 

340 

341 @classmethod 

342 def _from_db(self, name, pyrange, conn_or_curs): 

343 """Return a `RangeCaster` instance for the type *pgrange*. 

344 

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) 

350 

351 if conn.info.server_version < 90200: 

352 raise ProgrammingError("range types not available in version %s" 

353 % conn.info.server_version) 

354 

355 # Store the transaction status of the connection to revert it after use 

356 conn_status = conn.status 

357 

358 # Use the correct schema 

359 if '.' in name: 

360 schema, tname = name.split('.', 1) 

361 else: 

362 tname = name 

363 schema = 'public' 

364 

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() 

374 

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 

387 

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") 

404 

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() 

408 

409 if not rec: 

410 raise ProgrammingError( 

411 f"PostgreSQL range '{name}' not found") 

412 

413 type, subtype, array = rec[:3] 

414 

415 return RangeCaster(name, pyrange, 

416 oid=type, subtype_oid=subtype, array_oid=array) 

417 

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) 

431 

432 _re_undouble = re.compile(r'(["\\])\1') 

433 

434 def parse(self, s, cur=None): 

435 if s is None: 

436 return None 

437 

438 if s == 'empty': 

439 return self.range(empty=True) 

440 

441 m = self._re_range.match(s) 

442 if m is None: 

443 raise InterfaceError(f"failed to parse range: '{s}'") 

444 

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) 

450 

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) 

456 

457 if cur is not None: 

458 lower = cur.cast(self.subtype_oid, lower) 

459 upper = cur.cast(self.subtype_oid, upper) 

460 

461 bounds = m.group(1) + m.group(6) 

462 

463 return self.range(lower, upper, bounds) 

464 

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) 

469 

470 register_adapter(self.range, self.adapter) 

471 

472 

473class NumericRange(Range): 

474 """A `Range` suitable to pass Python numeric types to a PostgreSQL range. 

475 

476 PostgreSQL types :sql:`int4range`, :sql:`int8range`, :sql:`numrange` are 

477 casted into `!NumericRange` instances. 

478 """ 

479 pass 

480 

481 

482class DateRange(Range): 

483 """Represents :sql:`daterange` values.""" 

484 pass 

485 

486 

487class DateTimeRange(Range): 

488 """Represents :sql:`tsrange` values.""" 

489 pass 

490 

491 

492class DateTimeTZRange(Range): 

493 """Represents :sql:`tstzrange` values.""" 

494 pass 

495 

496 

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 

501 

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'" 

508 

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 = '' 

517 

518 if not r.upper_inf: 

519 upper = adapt(r.upper).getquoted().decode('ascii') 

520 else: 

521 upper = '' 

522 

523 return (f"'{r._bounds[0]}{lower},{upper}{r._bounds[1]}'").encode('ascii') 

524 

525 

526# TODO: probably won't work with infs, nans and other tricky cases. 

527register_adapter(NumericRange, NumberRangeAdapter) 

528 

529# Register globally typecasters and adapters for builtin range types. 

530 

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() 

535 

536int8range_caster = RangeCaster(NumberRangeAdapter, NumericRange, 

537 oid=3926, subtype_oid=20, array_oid=3927) 

538int8range_caster._register() 

539 

540numrange_caster = RangeCaster(NumberRangeAdapter, NumericRange, 

541 oid=3906, subtype_oid=1700, array_oid=3907) 

542numrange_caster._register() 

543 

544daterange_caster = RangeCaster('daterange', DateRange, 

545 oid=3912, subtype_oid=1082, array_oid=3913) 

546daterange_caster._register() 

547 

548tsrange_caster = RangeCaster('tsrange', DateTimeRange, 

549 oid=3908, subtype_oid=1114, array_oid=3909) 

550tsrange_caster._register() 

551 

552tstzrange_caster = RangeCaster('tstzrange', DateTimeTZRange, 

553 oid=3910, subtype_oid=1184, array_oid=3911) 

554tstzrange_caster._register()