init
This commit is contained in:
0
__init__.py
Normal file
0
__init__.py
Normal file
249
postgresmanager.py
Normal file
249
postgresmanager.py
Normal file
@@ -0,0 +1,249 @@
|
||||
import psycopg
|
||||
import logging
|
||||
import json
|
||||
from psycopg import sql
|
||||
from psycopg.rows import dict_row, DictRow, TupleRow
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class PostgresManager:
|
||||
"""Python wrapper for the psycopg API to streamline interactions with MAGNET Databases. When the object
|
||||
gets deleted, it will explicitly close the instance's connection to the underlying database
|
||||
|
||||
Returns:
|
||||
object: PostgresManager object with an ACTIVE Connection to the database.
|
||||
"""
|
||||
def __init__(self, host:str = "",
|
||||
port:int = 5432,
|
||||
database:str = "",
|
||||
user:str = "",
|
||||
password:str = ""):
|
||||
log.debug('Creating PSQL connection...')
|
||||
self.__conn = psycopg.connect(f"host={host} port={port} dbname={database} user={user} password={password}",
|
||||
row_factory=dict_row,
|
||||
autocommit=True)
|
||||
|
||||
def __del__(self):
|
||||
log.info("Closing PSQL connection...")
|
||||
del self.__conn
|
||||
|
||||
def insert(self, table:str, data:Dict[str,Any])->bool:
|
||||
""" Insert into a given table the dictionary of Column/Value pairs
|
||||
|
||||
Args:
|
||||
table (str): Table name
|
||||
data (Dict[str,Any]): Column/Value dictionary (e.g { "Col1":"Value1" })
|
||||
|
||||
Returns:
|
||||
bool: True if insert was successful, false if otherwise
|
||||
Example:
|
||||
To insert into the 'names' table, pass data={"name":"sally"}. Equivalent of
|
||||
SQL Query 'INSERT INTO names(name) VALUES(sally);'
|
||||
"""
|
||||
log.info(f"inserting values into {table} table")
|
||||
log.debug(f"data: {data}")
|
||||
|
||||
# JSON Serialize any Dicts because psql doesn't understand python dicts
|
||||
for k,v in data.items():
|
||||
if isinstance(v, dict):
|
||||
|
||||
# If it is a dict, if empty serialize otherwise do not or it will put in random garbage
|
||||
if bool(v):
|
||||
v = json.dumps(v)
|
||||
data[k] = v # Override with json string
|
||||
else:
|
||||
data[k] = ""
|
||||
|
||||
|
||||
columns = sql.SQL(", ").join(map(sql.Identifier, data.keys()))
|
||||
values = [value for value in data.values()]
|
||||
log.debug(f"values: {values}")
|
||||
query = sql.SQL("INSERT INTO {table} ({cols}) VALUES ({placeholders})").format(
|
||||
table=sql.Identifier(table),
|
||||
cols=columns,
|
||||
placeholders=sql.SQL(", ").join(sql.Placeholder() * len(data.values())))
|
||||
|
||||
log.debug(f'SQL Query: {query.as_string(self.__conn)}')
|
||||
with self.__conn.cursor() as cur:
|
||||
try:
|
||||
cur.execute(query,values)
|
||||
|
||||
except psycopg.DatabaseError as de:
|
||||
log.error(f"Database error: {de}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
log.critical(f"Critical error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
#self.conn.commit()
|
||||
logging.info("Insert successful")
|
||||
logging.debug("Exiting DB insert...")
|
||||
return True
|
||||
|
||||
def delete(self, table:str, where:dict[str,Any])->bool:
|
||||
""" DELETES from a given table.
|
||||
|
||||
Args:
|
||||
table (str): Table to delete from
|
||||
where (dict[str,Any]): where condition that represents LEFT_HAND = RIGHT_HAND.
|
||||
example: if you want to say 'DELETE FROM orchid WHERE fruit = apple', you could pass where={"fruit":"apple"}
|
||||
|
||||
Returns:
|
||||
bool: true if the transaction was successful, false if otherwise
|
||||
"""
|
||||
log.info(f"Deleting from {table} table")
|
||||
log.debug(f"where_condition: {where}")
|
||||
col = list(where.keys())[0]
|
||||
query = sql.SQL("DELETE FROM {table} WHERE {col_cond} = %s").format(
|
||||
table=sql.Identifier(table),
|
||||
col_cond=sql.Identifier(table,list(where.keys())[0])
|
||||
)
|
||||
|
||||
log.debug(f'SQL Query: {query.as_string(self.__conn)}')
|
||||
with self.__conn.cursor() as cur:
|
||||
try:
|
||||
cur.execute(query,[where.get(col)])
|
||||
|
||||
except psycopg.DatabaseError as de:
|
||||
log.error(f"Database error: {de}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
log.critical(f"Critical error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
#self.conn.commit()
|
||||
return True
|
||||
|
||||
def select(self, table:str, columns:list, where:Dict[str,Any] | None = None)->DictRow | None:
|
||||
""" A select query that retrieves values from passed columns and accept a where condition
|
||||
|
||||
Args:
|
||||
table (str): Table to select values from
|
||||
columns (list): The column names of the values you want from
|
||||
where (Dict[str,Any] | None, optional): A dictionary representing LEFT_HAND = RIGHT_HAND where condition
|
||||
|
||||
Returns:
|
||||
Union[Dict[str,Any]|None]: A [Dict[str,Any]] if transaction was successful, None if otherwise.
|
||||
"""
|
||||
logging.debug(f"Table: {table}")
|
||||
logging.debug(f'Where Condition: {where}')
|
||||
|
||||
|
||||
with self.__conn.cursor() as cur:
|
||||
try:
|
||||
r = []
|
||||
if where:
|
||||
logging.debug("Triggered SELECT with WHERE")
|
||||
where_key = list(where.keys())[0]
|
||||
where_value = where.get(where_key)
|
||||
logging.debug(f'Where_cond key = {where_key}, value = {where.get(where_key)}')
|
||||
query = sql.SQL("SELECT {cols} FROM {table_name} WHERE {where_cond} = %s").format(
|
||||
cols=sql.SQL(", ").join(map(sql.Identifier, columns)),
|
||||
table_name = sql.Identifier('public',table),
|
||||
where_cond = sql.Identifier(where_key)
|
||||
)
|
||||
log.debug(f'SQL Query: {query.as_string(self.__conn)}')
|
||||
|
||||
cur.execute(query,(where_value,))
|
||||
r = cur.fetchall()
|
||||
log.debug(f'r value = {r}')
|
||||
else:
|
||||
logging.debug("Triggered ONLY SELECT")
|
||||
query = sql.SQL("SELECT {cols} FROM {table_name}").format(
|
||||
cols=sql.SQL(", ").join(map(sql.Identifier, columns)),
|
||||
table_name = sql.Identifier(table),
|
||||
)
|
||||
logging.debug(f"SQL QUERY: {query.as_string(self.__conn)}")
|
||||
cur.execute(query)
|
||||
r = cur.fetchall()
|
||||
log.debug(f'r value = {r}')
|
||||
except psycopg.DatabaseError as de:
|
||||
log.error(f"Database error: {de}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
log.critical(f"Critical error: {e}")
|
||||
return None
|
||||
|
||||
log.debug(f"SELECT returned: {r}")
|
||||
return r
|
||||
|
||||
def update(self, table, set: dict[str, Any]={}, where: dict[str, Any]={}, pkey_name: str='id')->int | str:
|
||||
""" Updates a record based on the set and where conditions passed.
|
||||
|
||||
Args:
|
||||
table (_type_): Table to apply the update in
|
||||
set (dict[str, Any], optional): Column/Value pair, of what update values to SET. Defaults to {}.
|
||||
where (dict[str, Any], optional): where condition to do the UPDATE. Defaults to {}.
|
||||
|
||||
Returns:
|
||||
int | str: Returns the ID of the updated record
|
||||
"""
|
||||
log.info(f"Updating record in {table} table")
|
||||
log.debug(f"set: {set}")
|
||||
log.debug(f"where_condition: {where}")
|
||||
|
||||
# JSON Serialize any Dicts because psql doesn't understand python dicts
|
||||
for k,v in set.items():
|
||||
if isinstance(v, dict):
|
||||
# If not an empty dict then serialize otherwise it will put in random garbage
|
||||
if bool(v):
|
||||
v = json.dumps(v)
|
||||
set[k] = v # Override with json string
|
||||
else:
|
||||
set[k] = ""
|
||||
|
||||
col = list(where.keys())[0]
|
||||
set_clause_strings = []
|
||||
log.debug(f'set values = {set.values()}')
|
||||
where_clause_strings = []
|
||||
set_where_values = []
|
||||
|
||||
# Build the SET key = %s string
|
||||
for key,value in set.items():
|
||||
set_clause_strings.append(f"{sql.Identifier(key).as_string()} = {sql.Placeholder().as_string()}")
|
||||
set_where_values.append(value)
|
||||
logging.debug(f'set clause = {set_clause_strings}')
|
||||
|
||||
if len(where.keys()) == 1:
|
||||
for key,value in where.items():
|
||||
where_clause_strings.append(f"{sql.Identifier(key).as_string()} = {sql.Placeholder().as_string()}")
|
||||
set_where_values.append(value)
|
||||
else:
|
||||
for key,value in where.items():
|
||||
where_clause_strings.append(f"{sql.Identifier(key).as_string()} = {sql.Placeholder().as_string()}")
|
||||
set_where_values.append(value)
|
||||
logging.debug(f'where clause = {where_clause_strings}')
|
||||
|
||||
logging.debug(f'set_where_values = {set_where_values}')
|
||||
|
||||
query = sql.SQL("UPDATE {table} SET {lhs} WHERE {col_conds} RETURNING {pkey}").format(
|
||||
table=sql.Identifier("public",table),
|
||||
lhs=sql.SQL(', '.join(set for set in set_clause_strings)),
|
||||
col_conds=sql.SQL(' AND '.join(where for where in where_clause_strings)),
|
||||
pkey=sql.Identifier(table,pkey_name),
|
||||
)
|
||||
|
||||
logging.debug(f"SQL QUERY: {query.as_string(self.__conn)}")
|
||||
with self.__conn.cursor() as cur:
|
||||
r: dict = {}
|
||||
try:
|
||||
cur.execute(query,set_where_values)
|
||||
r = cur.fetchone()
|
||||
logging.debug(f'the value of r is: {r}')
|
||||
if r is None:
|
||||
raise psycopg.DatabaseError('Update affected no rows')
|
||||
|
||||
except psycopg.DatabaseError as de:
|
||||
log.error(f"Database error: {de}")
|
||||
return -1
|
||||
|
||||
except Exception as e:
|
||||
log.critical(f"Critical error: {e}")
|
||||
return -1
|
||||
return r[pkey_name]
|
||||
15
pyproject.toml
Normal file
15
pyproject.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[project]
|
||||
name = "postgresmanager"
|
||||
version = "0.1.0"
|
||||
description = "Wrapper library around Psycopg to simplify Postgres DB interaction"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"iniconfig==2.3.0",
|
||||
"packaging==25.0",
|
||||
"pluggy==1.6.0",
|
||||
"psycopg==3.2.11",
|
||||
"psycopg-binary==3.2.11",
|
||||
"pygments==2.19.2",
|
||||
"pytest==8.4.2",
|
||||
]
|
||||
9
pytest.ini
Normal file
9
pytest.ini
Normal file
@@ -0,0 +1,9 @@
|
||||
[pytest]
|
||||
minversion = 8.4.2
|
||||
addopts = -ra
|
||||
testpaths =
|
||||
tests
|
||||
log_cli = 1
|
||||
log_cli_level = DEBUG
|
||||
log_cli_date_format = %Y-%m-%d %H:%M:%S
|
||||
log_cli_format = %(asctime)s %(levelname)s %(message)s
|
||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
iniconfig==2.3.0
|
||||
packaging==25.0
|
||||
pluggy==1.6.0
|
||||
psycopg==3.2.11
|
||||
psycopg-binary==3.2.11
|
||||
Pygments==2.19.2
|
||||
pytest==8.4.2
|
||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
87
tests/test_pm.py
Normal file
87
tests/test_pm.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import logging
|
||||
from ..postgresmanager import PostgresManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def test_select_from_DB():
|
||||
logger.info('=== Running Test Select from DB ===')
|
||||
psql = PostgresManager(host="ingots-db-test", database="exploit_chain_catalog", user="ingots", password="ingots")
|
||||
resp = psql.select("otas",columns=['version', 'device_name'] )
|
||||
logger.info(f"Select returned: {resp}")
|
||||
assert resp
|
||||
|
||||
def test_select_with_where_from_DB():
|
||||
logger.info('=== Running Test Select with Where from DB ===')
|
||||
psql = PostgresManager(host="ingots-db-test", database="exploit_chain_catalog", user="ingots", password="ingots")
|
||||
resp = psql.select("otas",columns=['version', 'device_name'],where={'build_id':"BD4A.240925.111"})
|
||||
logger.info(f"Select returned: {resp}")
|
||||
assert len(resp) > 0
|
||||
|
||||
# Insert tests
|
||||
|
||||
def test_insert_into_DB():
|
||||
logger.info(' === Running Test insert into DB ===')
|
||||
psql = PostgresManager(host="ingots-db-test", database="exploit_chain_catalog", user="ingots", password="ingots")
|
||||
data = {"device": "test", "version": "test", "vendor":"test"}
|
||||
table = 'firmware'
|
||||
assert psql.insert(table,data)
|
||||
logger.info('Exiting Test insert into DB')
|
||||
|
||||
def test_insert_into_db_with_dict():
|
||||
logger.info('=== Running Test insert into DB === ')
|
||||
psql = PostgresManager(host="ingots-db-test", database="exploit_chain_catalog", user="ingots", password="ingots")
|
||||
data = {"device": "test2", "version": "test2", "vendor":{}}
|
||||
table = 'firmware'
|
||||
assert psql.insert(table,data)
|
||||
logger.info('Exiting Test insert into DB')
|
||||
|
||||
# Update tests
|
||||
def test_update_single_update_record():
|
||||
logger.info(' === Running Update single record === ')
|
||||
psql = PostgresManager(host="ingots-db-test", database="exploit_chain_catalog", user="ingots", password="ingots")
|
||||
set = {"version": "50"}
|
||||
where = {"device": "test"}
|
||||
table = 'firmware'
|
||||
assert psql.update(table, set, where) > 0
|
||||
logger.info('Exiting Test Update record')
|
||||
|
||||
def test_update_multiple_updates():
|
||||
logger.info(' === Running Test Update with Multiple Values === ')
|
||||
psql = PostgresManager(host="ingots-db-test", database="exploit_chain_catalog", user="ingots", password="ingots")
|
||||
set = {"device": "test3", 'version': 'test_string'}
|
||||
where = {'device':'test', 'version': '50'}
|
||||
table = 'firmware'
|
||||
assert psql.update(table,set,where=where) > 0
|
||||
|
||||
def test_update_with_dict_values():
|
||||
logger.info(' === Running Test Update with Dictionary value === ')
|
||||
psql = PostgresManager(host="ingots-db-test", database="exploit_chain_catalog", user="ingots", password="ingots")
|
||||
set = {"version": {'ios':'leopard'}}
|
||||
where = {'device':'test3', 'version': 'test_string'}
|
||||
table = 'firmware'
|
||||
assert psql.update(table,set,where=where) > 0
|
||||
|
||||
|
||||
def test_delete_from_DB():
|
||||
logger.info('=== Running Test deleting from table === ')
|
||||
psql = PostgresManager(host="ingots-db-test", database="exploit_chain_catalog", user="ingots", password="ingots")
|
||||
data = {"device": "test"}
|
||||
table = 'firmware'
|
||||
assert psql.delete(table,data)
|
||||
#data['device'] = 'test2'
|
||||
#assert psql.delete(table,data)
|
||||
data['device'] = 'test3'
|
||||
assert psql.delete(table,data)
|
||||
logger.info('Exiting Test deleting from table DB')
|
||||
|
||||
|
||||
def test_neg_update_record():
|
||||
logger.info('=== Running NEGATIVE Test Update record ===')
|
||||
psql = PostgresManager(host="ingots-db-test", database="exploit_chain_catalog", user="ingots", password="ingots")
|
||||
set = {"version": "50"}
|
||||
where = {"id": "88888888"}
|
||||
table = 'firmware'
|
||||
assert psql.update(table, set, where) == -1
|
||||
logger.info('Exiting NEGATIVE Test Update record')
|
||||
|
||||
154
uv.lock
generated
Normal file
154
uv.lock
generated
Normal file
@@ -0,0 +1,154 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "postgresmanager"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "psycopg" },
|
||||
{ name = "psycopg-binary" },
|
||||
{ name = "pygments" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "iniconfig", specifier = "==2.3.0" },
|
||||
{ name = "packaging", specifier = "==25.0" },
|
||||
{ name = "pluggy", specifier = "==1.6.0" },
|
||||
{ name = "psycopg", specifier = "==3.2.11" },
|
||||
{ name = "psycopg-binary", specifier = "==3.2.11" },
|
||||
{ name = "pygments", specifier = "==2.19.2" },
|
||||
{ name = "pytest", specifier = "==8.4.2" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg"
|
||||
version = "3.2.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/27/02/9fdfc018c026df2bcf9c11480c1014f9b90c6d801e5f929408cbfbf94cc0/psycopg-3.2.11.tar.gz", hash = "sha256:398bb484ed44361e041c8f804ed7af3d2fcefbffdace1d905b7446c319321706", size = 160644, upload-time = "2025-10-18T22:48:28.136Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/1b/96ee90ed0007d64936d9bd1bb3108d0af3cf762b4f11dbd73359f0687c3d/psycopg-3.2.11-py3-none-any.whl", hash = "sha256:217231b2b6b72fba88281b94241b2f16043ee67f81def47c52a01b72ff0c086a", size = 206766, upload-time = "2025-10-18T22:43:32.114Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg-binary"
|
||||
version = "3.2.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9e/58945c828b60820e5c192d04f238f1aa49de0fe5f3b9883e277f33c17c0a/psycopg_binary-3.2.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4cae9bdc482e36e825d5102a9f3010e729f33a4ca83fc8a1f439ba16eb61e1f1", size = 4019920, upload-time = "2025-10-18T22:45:05.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/c4/ac7f600ae5d8fb7a89c2712163b642d88739b3bb4c8d0fb3178c084dc521/psycopg_binary-3.2.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:749d23fbfd642a7abfef5fc0f6ca185fa82a2c0f895e6eab42c3f2a5d88f6011", size = 4092123, upload-time = "2025-10-18T22:45:09.763Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/aa/866c8b2c83490f0d55c4a27d16c0b733744faac442adf181eb59d8d48a3d/psycopg_binary-3.2.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58d8f9f80ae79ba7f2a0509424939236220d7d66a4f8256ae999b882cc58065b", size = 4626894, upload-time = "2025-10-18T22:45:13.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/a8/e7c1eba4ca230d510b76b3f8701321e0c21820953744db67ec7c8fb67537/psycopg_binary-3.2.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:eab6959fade522e586b8ec37d3fe337ce10861965edef3292f52e66e36dc375d", size = 4719913, upload-time = "2025-10-18T22:45:19.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/5f/de0dea38cef6e050ff8e9acd0f7c5d956251fcfece5360973329eb10b84b/psycopg_binary-3.2.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe5e3648e855df4fba1d70c18aef18c9880ea8d123fdfae754c18787c8cb37b3", size = 4411018, upload-time = "2025-10-18T22:45:24.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/bf/2bbefb24e491f2fa4a7c627d14680429ca33092176eadae88fab4fbce8c6/psycopg_binary-3.2.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:30e2c114d26554ae677088de5d4133cc112344d7a233200fdbf4a2ca5754c7ec", size = 3861940, upload-time = "2025-10-18T22:45:28.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/07/d68f78df7490fcd17eef7f138f96bf3398a961208262498cde7d30266481/psycopg_binary-3.2.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e3f5887019dfb094c60e7026968ca3a964ca16305807ba5e43f9a78483767d5f", size = 3534831, upload-time = "2025-10-18T22:45:32.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/18/fc5a881ca3d8b40b8e37a396bf14176b8439a7e4b1a29848af325009f955/psycopg_binary-3.2.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9b4b0fc4e774063ae64c92cc57e2b10160150de68c96d71743218159d953869d", size = 3583559, upload-time = "2025-10-18T22:45:36.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/98/c4418b609ffea80907861ddb01c043af860b179cb8fb41905ad2f0a4f400/psycopg_binary-3.2.11-cp312-cp312-win_amd64.whl", hash = "sha256:9bdc762600fcc8e4ad3224734a4e70cc226207fd8f2de47c36b115efeed01782", size = 2910294, upload-time = "2025-10-18T22:45:40.135Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/93/9cea78ed3b279909f0fd6c2badb24b2361b93c875d6a7c921e26f6254044/psycopg_binary-3.2.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47f6cf8a1d02d25238bdb8741ac641ff0ec22b1c6ff6a2acd057d0da5c712842", size = 4017939, upload-time = "2025-10-18T22:45:45.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/86/fc9925f500b2c140c0bb8c1f8fcd04f8c45c76d4852e87baf4c75182de8c/psycopg_binary-3.2.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91268f04380964a5e767f8102d05f1e23312ddbe848de1a9514b08b3fc57d354", size = 4090150, upload-time = "2025-10-18T22:45:50.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/10/752b698da1ca9e6c5f15d8798cb637c3615315fd2da17eee4a90cf20ee08/psycopg_binary-3.2.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:199f88a05dd22133eab2deb30348ef7a70c23d706c8e63fdc904234163c63517", size = 4625597, upload-time = "2025-10-18T22:45:54.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/9f/b578545c3c23484f4e234282d97ab24632a1d3cbfec64209786872e7cc8f/psycopg_binary-3.2.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7b3c5474dbad63bcccb8d14d4d4c7c19f1dc6f8e8c1914cbc771d261cf8eddca", size = 4720326, upload-time = "2025-10-18T22:45:59.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/3b/ba548d3fe65a7d4c96e568c2188e4b665802e3cba41664945ed95d16eae9/psycopg_binary-3.2.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:581358e770a4536e546841b78fd0fe318added4a82443bf22d0bbe3109cf9582", size = 4411647, upload-time = "2025-10-18T22:46:04.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/65/559ab485b198600e7ff70d70786ae5c89d63475ca01d43a7dda0d7c91386/psycopg_binary-3.2.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54a30f00a51b9043048b3e7ee806ffd31fc5fbd02a20f0e69d21306ff33dc473", size = 3863037, upload-time = "2025-10-18T22:46:08.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/29/05d0b48c8bef147e8216a36a1263a309a6240dcc09a56f5b8174fa6216d2/psycopg_binary-3.2.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2a438fad4cc081b018431fde0e791b6d50201526edf39522a85164f606c39ddb", size = 3536975, upload-time = "2025-10-18T22:46:12.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/75/304e133d3ab1a49602616192edb81f603ed574f79966449105f2e200999d/psycopg_binary-3.2.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f5e7415b5d0f58edf2708842c66605092df67f3821161d861b09695fc326c4de", size = 3586213, upload-time = "2025-10-18T22:46:19.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/10/c47cce42fa3c37d439e1400eaa5eeb2ce53dc3abc84d52c8a8a9e544d945/psycopg_binary-3.2.11-cp313-cp313-win_amd64.whl", hash = "sha256:6b9632c42f76d5349e7dd50025cff02688eb760b258e891ad2c6428e7e4917d5", size = 2912997, upload-time = "2025-10-18T22:46:24.978Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/13/728b4763ef76a688737acebfcb5ab8696b024adc49a69c86081392b0e5ba/psycopg_binary-3.2.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:260738ae222b41dbefd0d84cb2e150a112f90b41688630f57fdac487ab6d6f38", size = 4016962, upload-time = "2025-10-18T22:46:29.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/0f/6180149621a907c5b60a2fae87d6ee10cc13e8c9f58d8250c310634ced04/psycopg_binary-3.2.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c594c199869099c59c85b9f4423370b6212491fb929e7fcda0da1768761a2c2c", size = 4090614, upload-time = "2025-10-18T22:46:33.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/97/cce19bdef510b698c9036d5573b941b539ffcaa7602450da559c8a62e0c3/psycopg_binary-3.2.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5768a9e7d393b2edd3a28de5a6d5850d054a016ed711f7044a9072f19f5e50d5", size = 4629749, upload-time = "2025-10-18T22:46:37.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/9d/9bff18989fb2bf05d18c1431dd8bec4a1d90141beb11fc45d3269947ddf3/psycopg_binary-3.2.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:27eb6367350b75fef882c40cd6f748bfd976db2f8651f7511956f11efc15154f", size = 4724035, upload-time = "2025-10-18T22:46:42.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/e5/39b930323428596990367b7953197730213d3d9d07bcedcad1d026608178/psycopg_binary-3.2.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa2aa5094dc962967ca0978c035b3ef90329b802501ef12a088d3bac6a55598e", size = 4411419, upload-time = "2025-10-18T22:46:47.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/9c/97c25438d1e51ddc6a7f67990b4c59f94bc515114ada864804ccee27ef1b/psycopg_binary-3.2.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7744b4ed1f3b76fe37de7e9ef98014482fe74b6d3dfe1026cc4cfb4b4404e74f", size = 3867844, upload-time = "2025-10-18T22:46:53.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/51/8c1e291cf4aa9982666f71a886aa782d990aa16853a42de545a0a9a871ef/psycopg_binary-3.2.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5f6f948ff1cd252003ff534d7b50a2b25453b4212b283a7514ff8751bdb68c37", size = 3541539, upload-time = "2025-10-18T22:46:58.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/0a/e25edcdfa1111bfc5c95668b7469b5a957b40ce10cc81383688d65564826/psycopg_binary-3.2.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3bd2c8fb1dec6f93383fbaa561591fa3d676e079f9cb9889af17c3020a19715f", size = 3588090, upload-time = "2025-10-18T22:47:04.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/aa/f8c2f4b4c13d5680a20e5bfcd61f9e154bce26e7a2c70cb0abeade088d61/psycopg_binary-3.2.11-cp314-cp314-win_amd64.whl", hash = "sha256:c45f61202e5691090a697e599997eaffa3ec298209743caa4fd346145acabafe", size = 3006049, upload-time = "2025-10-18T22:47:07.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2025.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user