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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: umnetdb-utils
3
- Version: 0.1.4
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.1.4"
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,4 @@
1
1
  from .umnetequip import UMnetequip
2
2
  from .umnetinfo import UMnetinfo
3
3
  from .umnetdisco import Umnetdisco
4
- from .umnetdb import UMnetdb
4
+ from .umnetdb import UMnetdb
@@ -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(self, select, table, joins=None, where=None, order_by=None, limit=None, group_by=None, distinct=False) -> str:
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 = 'distinct ' if distinct else ''
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
- def execute(self, sql:str, rows_as_dict:bool=True, fetch_one:bool=False) -> Union[list[dict],dict]:
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
+