-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Issue 1325]: add python interactive console #1331
Changes from 9 commits
e7fd6b0
988e3a1
033c4e3
2293563
899146b
2753e9a
e300a00
8c5a71e
a5e7a92
f2ece98
13ca978
aad5683
6114ddf
0368325
22ea53e
8626a1f
430350c
4562c3e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
# | ||
# Interactive console for using database and services. | ||
# | ||
# Connects to the database then drops into a Python interactive prompt. | ||
# | ||
# Use via the `-i` flag to python, for example: | ||
# poetry run python3 -i -m src.tool.console.interactive | ||
# | ||
|
||
from types import ModuleType | ||
|
||
import rich | ||
import rich.panel | ||
import rich.pretty | ||
|
||
import src.adapters.db as db | ||
import src.db | ||
import src.db.models | ||
import src.logging | ||
import src.util | ||
import tests.src.db.models.factories | ||
from src.adapters.db.clients.postgres_client import PostgresDBClient | ||
from src.adapters.db.clients.postgres_config import get_db_config | ||
|
||
INTRO = """ | ||
Simpler Grants Gov Python console | ||
|
||
Useful things: | ||
|
||
│ db = database session | ||
│ f = DB factories module | ||
│ u = utilities module | ||
│ r = function to reload REPL | ||
|
||
Tips: | ||
★ Tab-completion is available | ||
★ History is available (use ↑↓ keys) | ||
★ Use Ctrl+D to exit | ||
""" | ||
|
||
|
||
def interactive_console() -> dict: | ||
"""Set up variables and print a introduction message for the interactive console.""" | ||
|
||
db_session = connect_to_database() | ||
|
||
print(INTRO.format(**locals())) | ||
|
||
variables = dict() | ||
variables.update(vars(src.db.models.opportunity_models)) | ||
variables.update(vars(src.db.models.lookup_models)) | ||
|
||
# This goes after the imports of entire modules, so the console reserved | ||
# names (db, fineos, etc) take precedence. This might break some modules | ||
# that expect something different under those names. | ||
variables.update(locals()) | ||
|
||
# DB | ||
variables["db_session"] = db_session | ||
variables["dbs"] = db_session | ||
|
||
# DB Factories | ||
factories_module = tests.src.db.models.factories | ||
if isinstance(db_session, db.Session): | ||
factories_module._db_session = db_session | ||
variables["f"] = tests.src.db.models.factories | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI - we might want to adjust where this utility lives - I made it so the factories are only a test component with the dependencies as dev only. We shouldn't be importing them into the I'm not sure if the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm open but would propose splitting that piece out to a separate PR. I do think it would make sense to close the loop on the intercepting session errors though - so that we don't have to manually do a rollback when it gets to an error state There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I looked into the rollback automation, not finding a ton of approaches. Tried rolling back automatically when an error happens and that just cascades into other errors. In short, SQLAlchemy changed sessions to generally be made for handling a single set of transactions (great for an API, less so for an interactive console). Without thinking through it much, the right approach probably involves lambdas/sessionmakers to automatically make new sessions when old ones error, but that's likely something that will take a while to figure out There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Want to just approve and merge then after fixing the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, we can put that somewhere in the backlog, just adjust the repr bit and we can go from there |
||
|
||
# Easy access to utilities | ||
variables["u"] = src.util | ||
variables["util"] = src.util | ||
|
||
# Easy reloading of modules imported in REPL, for retrying something after a | ||
# code change without dropping out of REPL | ||
variables["r"] = reload_repl | ||
variables["reload"] = reload_repl | ||
variables["reload_module"] = reload_module | ||
|
||
return variables | ||
|
||
|
||
def connect_to_database() -> db.Session | Exception: | ||
db_config = get_db_config() | ||
|
||
# errors sometimes dump sensitive info | ||
# (since we're doing locally, we don't need to hide) | ||
db_config.hide_sql_parameter_logs = False | ||
db_session: db.Session | Exception | ||
try: | ||
db_session = PostgresDBClient(db_config).get_session() | ||
except Exception as err: | ||
db_session = err | ||
|
||
return db_session | ||
|
||
|
||
def reload_repl() -> None: | ||
import importlib | ||
from sys import modules | ||
|
||
for module in set(modules.values()): | ||
# only reload our code | ||
if "<module 'src." not in str(module): | ||
continue | ||
|
||
# individual database model modules can be particular in how they are | ||
# loaded, so don't automatically reload them | ||
if "<module 'src.db.models." in str(module): | ||
continue | ||
|
||
# reloading the logging initialization and stuff can cause some issues, | ||
# avoid it all for now | ||
if "<module 'src.util.logging" in str(module): | ||
continue | ||
|
||
try: | ||
importlib.reload(module) | ||
except: # noqa: B001 | ||
# there are some problems that are swept under the rug here | ||
pass | ||
|
||
|
||
def reload_module(m: ModuleType) -> None: | ||
import importlib | ||
|
||
importlib.reload(m) | ||
|
||
|
||
if __name__ == "__main__": | ||
with src.logging.init(__package__): | ||
interactive_variables = interactive_console() | ||
globals().update(interactive_variables) | ||
rich.pretty.install(indent_guides=True, max_length=20, max_string=400) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@chouinar - hope its ok to add repr methods on the db models so printing is a little bit cleaner - where before it was just printing the object and mem address (I was having call a
vars(object)
on it to see the values). Maybe there's a better way though than manually spelling it out for each model though.