umnetdb-utils 0.1.4__tar.gz → 0.2.0__tar.gz
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.
- {umnetdb_utils-0.1.4 → umnetdb_utils-0.2.0}/PKG-INFO +2 -1
- {umnetdb_utils-0.1.4 → umnetdb_utils-0.2.0}/pyproject.toml +6 -2
- {umnetdb_utils-0.1.4 → umnetdb_utils-0.2.0}/umnetdb_utils/__init__.py +1 -1
- {umnetdb_utils-0.1.4 → umnetdb_utils-0.2.0}/umnetdb_utils/base.py +28 -17
- umnetdb_utils-0.2.0/umnetdb_utils/cli.py +114 -0
- umnetdb_utils-0.2.0/umnetdb_utils/umnetdb.py +418 -0
- {umnetdb_utils-0.1.4 → umnetdb_utils-0.2.0}/umnetdb_utils/umnetdisco.py +84 -64
- {umnetdb_utils-0.1.4 → umnetdb_utils-0.2.0}/umnetdb_utils/umnetequip.py +90 -83
- {umnetdb_utils-0.1.4 → umnetdb_utils-0.2.0}/umnetdb_utils/umnetinfo.py +3 -6
- umnetdb_utils-0.2.0/umnetdb_utils/utils.py +323 -0
- umnetdb_utils-0.1.4/umnetdb_utils/umnetdb.py +0 -118
- umnetdb_utils-0.1.4/umnetdb_utils/utils.py +0 -39
- {umnetdb_utils-0.1.4 → umnetdb_utils-0.2.0}/README.md +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: umnetdb-utils
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.0
|
4
4
|
Summary: Helper classes for querying UMnet databases
|
5
5
|
License: MIT
|
6
6
|
Author: Amy Liebowitz
|
@@ -16,6 +16,7 @@ Requires-Dist: oracledb (>=3.1.0,<4.0.0)
|
|
16
16
|
Requires-Dist: psycopg[binary] (>=3.2.9,<4.0.0)
|
17
17
|
Requires-Dist: python-decouple (>=3.8,<4.0)
|
18
18
|
Requires-Dist: sqlalchemy (>=2.0.41,<3.0.0)
|
19
|
+
Requires-Dist: typer (>=0.16.0,<0.17.0)
|
19
20
|
Description-Content-Type: text/markdown
|
20
21
|
|
21
22
|
# umnetdb-utils
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "umnetdb-utils"
|
3
|
-
version = "0.
|
3
|
+
version = "0.2.0"
|
4
4
|
description = "Helper classes for querying UMnet databases"
|
5
5
|
authors = [
|
6
6
|
{name = "Amy Liebowitz",email = "amylieb@umich.edu"}
|
@@ -12,9 +12,13 @@ dependencies = [
|
|
12
12
|
"sqlalchemy (>=2.0.41,<3.0.0)",
|
13
13
|
"oracledb (>=3.1.0,<4.0.0)",
|
14
14
|
"psycopg[binary] (>=3.2.9,<4.0.0)",
|
15
|
-
"python-decouple (>=3.8,<4.0)"
|
15
|
+
"python-decouple (>=3.8,<4.0)",
|
16
|
+
"typer (>=0.16.0,<0.17.0)"
|
16
17
|
]
|
17
18
|
|
19
|
+
[project.scripts]
|
20
|
+
umnetdb='umnetdb_utils.cli:main'
|
21
|
+
|
18
22
|
[tool.poetry]
|
19
23
|
|
20
24
|
[tool.poetry.group.dev.dependencies]
|
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
from typing import Union
|
3
2
|
from os import getenv
|
4
3
|
import re
|
@@ -10,14 +9,16 @@ from sqlalchemy.orm import Session
|
|
10
9
|
|
11
10
|
logger = logging.getLogger(__name__)
|
12
11
|
|
12
|
+
|
13
13
|
class UMnetdbBase:
|
14
14
|
"""
|
15
15
|
Base helper class
|
16
16
|
"""
|
17
|
+
|
17
18
|
# set in child classes - you can use environment variables within curly braces here
|
18
19
|
URL = None
|
19
20
|
|
20
|
-
def __init__(self, env_file:str=".env"):
|
21
|
+
def __init__(self, env_file: str = ".env"):
|
21
22
|
"""
|
22
23
|
Initiate a umnetdb object. Optionally provide a path to a file with environment variables
|
23
24
|
containing the credentials for the database. If no file is provided and there's no ".env",
|
@@ -73,14 +74,24 @@ class UMnetdbBase:
|
|
73
74
|
def __exit__(self, fexc_type, exc_val, exc_tb):
|
74
75
|
self.close()
|
75
76
|
|
76
|
-
def __getattr__(self, val:str):
|
77
|
+
def __getattr__(self, val: str):
|
77
78
|
if self.session:
|
78
79
|
return getattr(self.session, val)
|
79
80
|
|
80
81
|
raise AttributeError(self)
|
81
82
|
|
82
|
-
def _build_select(
|
83
|
-
|
83
|
+
def _build_select(
|
84
|
+
self,
|
85
|
+
select,
|
86
|
+
table,
|
87
|
+
joins=None,
|
88
|
+
where=None,
|
89
|
+
order_by=None,
|
90
|
+
limit=None,
|
91
|
+
group_by=None,
|
92
|
+
distinct=False,
|
93
|
+
) -> str:
|
94
|
+
"""
|
84
95
|
Generic 'select' query string builder built from standard query components as input.
|
85
96
|
The user is required to generate substrings for the more complex inputs
|
86
97
|
(eg joins, where statements), this function just puts all the components
|
@@ -93,7 +104,7 @@ class UMnetdbBase:
|
|
93
104
|
ex: "node_ip nip"
|
94
105
|
:joins: a list of strings representing join statements. Include the actual 'join' part!
|
95
106
|
ex: ["join node n on nip.mac = n.mac", "join device d on d.ip = n.switch"]
|
96
|
-
:where: For a single where statement, provide a string. For multiple provide a list.
|
107
|
+
:where: For a single where statement, provide a string. For multiple provide a list.
|
97
108
|
The list of statements are "anded". If you need "or", embed it in one of your list items
|
98
109
|
DO NOT provide the keyword 'where' - it is auto-added.
|
99
110
|
ex: ["node_ip.ip = '1.2.3.4'", "node.switch = '10.233.0.5'"]
|
@@ -101,10 +112,10 @@ class UMnetdbBase:
|
|
101
112
|
:group_by: A string representing a column name (or names) to group by
|
102
113
|
:limit: An integer
|
103
114
|
|
104
|
-
|
115
|
+
"""
|
105
116
|
|
106
117
|
# First part of the sql statement is the 'select'
|
107
|
-
distinct =
|
118
|
+
distinct = "distinct " if distinct else ""
|
108
119
|
sql = f"select {distinct}" + ", ".join(select) + "\n"
|
109
120
|
|
110
121
|
# Next is the table
|
@@ -118,7 +129,6 @@ class UMnetdbBase:
|
|
118
129
|
|
119
130
|
# Next are the filters. They are 'anded'
|
120
131
|
if where and isinstance(where, list):
|
121
|
-
|
122
132
|
sql += "where\n"
|
123
133
|
sql += " and\n".join(where) + "\n"
|
124
134
|
elif where:
|
@@ -133,20 +143,20 @@ class UMnetdbBase:
|
|
133
143
|
|
134
144
|
if limit:
|
135
145
|
sql += f"limit {limit}\n"
|
136
|
-
|
146
|
+
|
137
147
|
logger.debug(f"Generated SQL command:\n****\n{sql}\n****\n")
|
138
148
|
|
139
149
|
return sql
|
140
150
|
|
141
|
-
def _execute(self, sql:str, rows_as_dict:bool=True, fetch_one:bool=False):
|
142
|
-
|
151
|
+
def _execute(self, sql: str, rows_as_dict: bool = True, fetch_one: bool = False):
|
152
|
+
"""
|
143
153
|
Generic sqlalchemy "open a session, execute this sql command and give me all the results"
|
144
154
|
|
145
155
|
NB This function is defined for legacy database classes that came from umnet-scripts.
|
146
156
|
It's encouraged to use "self.session.execute" in other child methods, allowing
|
147
157
|
scripts that import the child class to use the context manager and execute multiple
|
148
158
|
mehtods within the same session.
|
149
|
-
|
159
|
+
"""
|
150
160
|
with self.engine.begin() as c:
|
151
161
|
r = c.execute(text(sql))
|
152
162
|
|
@@ -158,8 +168,9 @@ class UMnetdbBase:
|
|
158
168
|
else:
|
159
169
|
return []
|
160
170
|
|
161
|
-
|
162
|
-
|
171
|
+
def execute(
|
172
|
+
self, sql: str, rows_as_dict: bool = True, fetch_one: bool = False
|
173
|
+
) -> Union[list[dict], dict]:
|
163
174
|
"""
|
164
175
|
Executes a sqlalchemy command and gives all the results as a list of dicts, or as a dict
|
165
176
|
if 'fetch_one' is set to true.
|
@@ -172,5 +183,5 @@ class UMnetdbBase:
|
|
172
183
|
|
173
184
|
if fetch_one:
|
174
185
|
return dict(result.fetchone())
|
175
|
-
|
176
|
-
return [dict(r) for r in result.fetchall()]
|
186
|
+
|
187
|
+
return [dict(r) for r in result.fetchall()]
|
@@ -0,0 +1,114 @@
|
|
1
|
+
import typer
|
2
|
+
from umnetdb_utils import UMnetdb
|
3
|
+
import inspect
|
4
|
+
from functools import wraps
|
5
|
+
|
6
|
+
from rich.console import Console
|
7
|
+
from rich.table import Table
|
8
|
+
|
9
|
+
from typing import Callable, List
|
10
|
+
from typing_extensions import Annotated
|
11
|
+
|
12
|
+
app = typer.Typer()
|
13
|
+
|
14
|
+
def print_result(result:List[dict]):
|
15
|
+
"""
|
16
|
+
Takes the result of a umnetdb call and prints it as a table
|
17
|
+
"""
|
18
|
+
if len(result) == 0:
|
19
|
+
print("No results found")
|
20
|
+
return
|
21
|
+
|
22
|
+
if isinstance(result, dict):
|
23
|
+
result = [result]
|
24
|
+
|
25
|
+
# instantiate table with columns based on entry dict keys
|
26
|
+
table = Table(*result[0].keys())
|
27
|
+
for row in result:
|
28
|
+
table.add_row(*[str(i) for i in row.values()])
|
29
|
+
|
30
|
+
console = Console()
|
31
|
+
console.print(table)
|
32
|
+
|
33
|
+
|
34
|
+
def command_generator(method_name:str, method:Callable):
|
35
|
+
"""
|
36
|
+
Generates a typer command function for an arbitrary method
|
37
|
+
in the umnetdb class. The generated function opens a connection with
|
38
|
+
the database, executes the method, and prints out the results.
|
39
|
+
|
40
|
+
Note that the docstring of each method is interrogated to generate
|
41
|
+
help text for each typer command.
|
42
|
+
|
43
|
+
:method_name: The name of the method
|
44
|
+
:method: The method itself
|
45
|
+
"""
|
46
|
+
|
47
|
+
# first we're going to tease out the 'help' portions of the method
|
48
|
+
# from the docstring.
|
49
|
+
docstr = method.__doc__
|
50
|
+
docstr_parts = docstr.split("\n:")
|
51
|
+
|
52
|
+
# first section of the docstring is always a generic 'this is what the method does'.
|
53
|
+
cmd_help = docstr_parts.pop(0)
|
54
|
+
|
55
|
+
# next sections are details on the specific arguments that we want to pass to typer as
|
56
|
+
# special annotated type hints
|
57
|
+
arg_help = {}
|
58
|
+
for arg_str in docstr_parts:
|
59
|
+
if ":" in arg_str:
|
60
|
+
arg, help = arg_str.split(":")
|
61
|
+
arg_help[arg] = help.strip()
|
62
|
+
|
63
|
+
sig = inspect.signature(method)
|
64
|
+
|
65
|
+
# going through the method's arguments and augmenting the 'help' section for each one
|
66
|
+
# from the docstring if applicable
|
67
|
+
new_params = []
|
68
|
+
for p_name, p in sig.parameters.items():
|
69
|
+
|
70
|
+
# need to skip self
|
71
|
+
if p_name == "self":
|
72
|
+
continue
|
73
|
+
|
74
|
+
# if there wasn't any helper text then just append the parameter as is
|
75
|
+
if p_name not in arg_help:
|
76
|
+
new_params.append(p)
|
77
|
+
continue
|
78
|
+
|
79
|
+
# params without default values should be typer 'arguments'
|
80
|
+
if p.default == inspect._empty:
|
81
|
+
new_params.append(p.replace(annotation=Annotated[p.annotation, typer.Argument(help=arg_help[p_name])]))
|
82
|
+
continue
|
83
|
+
|
84
|
+
# params with default values should be typer 'options'
|
85
|
+
new_params.append(p.replace(annotation=Annotated[p.annotation, typer.Option(help=arg_help[p_name])]))
|
86
|
+
|
87
|
+
new_sig = sig.replace(parameters=new_params)
|
88
|
+
|
89
|
+
|
90
|
+
# new munged function based on the origional method, with a new signature
|
91
|
+
# and docstring for typer
|
92
|
+
@wraps(method)
|
93
|
+
def wrapper(*args, **kwargs):
|
94
|
+
with UMnetdb() as db:
|
95
|
+
result = getattr(db, method_name)(*args, **kwargs)
|
96
|
+
print_result(result)
|
97
|
+
|
98
|
+
wrapper.__signature__ = new_sig
|
99
|
+
wrapper.__doc__ = cmd_help
|
100
|
+
|
101
|
+
return wrapper
|
102
|
+
|
103
|
+
|
104
|
+
def main():
|
105
|
+
for f_name,f in UMnetdb.__dict__.items():
|
106
|
+
if not(f_name.startswith("_")) and callable(f):
|
107
|
+
app.command()(command_generator(f_name, f))
|
108
|
+
|
109
|
+
app()
|
110
|
+
|
111
|
+
if __name__ == "__main__":
|
112
|
+
main()
|
113
|
+
|
114
|
+
|