From 80070044565bcd4542225efb9a4cc47e31fe76dc Mon Sep 17 00:00:00 2001 From: Ryan Lewis <93001277+rylew1@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:17:52 -0800 Subject: [PATCH] [Issue 1325]: add python interactive console (#1331) Summary Fixes #1325 Time to review: 5 min add make console target that loads Python interactive console --- api/Makefile | 3 + api/src/db/models/base.py | 7 ++ api/src/tool/__init__.py | 0 api/src/tool/console/__init__.py | 0 api/src/tool/console/interactive.py | 132 ++++++++++++++++++++++++++++ documentation/api/development.md | 4 + 6 files changed, 146 insertions(+) create mode 100644 api/src/tool/__init__.py create mode 100644 api/src/tool/console/__init__.py create mode 100644 api/src/tool/console/interactive.py diff --git a/api/Makefile b/api/Makefile index 28a28952c..4d344be89 100644 --- a/api/Makefile +++ b/api/Makefile @@ -250,5 +250,8 @@ copy-oracle-data: login: start ## Start shell in running container docker exec -it $(APP_NAME) bash +console: ## Start interactive Python console + $(PY_RUN_CMD) python3 -i -m src.tool.console.interactive + help: ## Prints the help documentation and info about each command @grep -E '^[/a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/api/src/db/models/base.py b/api/src/db/models/base.py index debfbb6bf..d4c38e42b 100644 --- a/api/src/db/models/base.py +++ b/api/src/db/models/base.py @@ -81,6 +81,13 @@ def copy(self, **kwargs: dict[str, Any]) -> "Base": copy = self.__class__(**data) return copy + def __repr__(self) -> str: + values = [] + for k, v in self.for_json().items(): + values.append(f"{k}={v!r}") + + return f"<{self.__class__.__name__}({','.join(values)})" + @declarative_mixin class IdMixin: diff --git a/api/src/tool/__init__.py b/api/src/tool/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/src/tool/console/__init__.py b/api/src/tool/console/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/src/tool/console/interactive.py b/api/src/tool/console/interactive.py new file mode 100644 index 000000000..9c286432b --- /dev/null +++ b/api/src/tool/console/interactive.py @@ -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 + + # 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 " 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) diff --git a/documentation/api/development.md b/documentation/api/development.md index 8a44a98fa..3e8b9d563 100644 --- a/documentation/api/development.md +++ b/documentation/api/development.md @@ -40,6 +40,10 @@ A very simple [docker-compose.yml](../../docker-compose.yml) has been included t See the [Makefile](../../api/Makefile) for a full list of commands you can run. +The `make console` command initializes a Python REPL environment pre-configured with database connectivity. This allows developers to perform database queries, utilize factories for data generation, and interact with the application's models directly. +- Writing a query: `dbs.query(Opportunity).all()` +- Saving some factory generated data to the db: `f.OpportunityFactory.create()` + ## Docker and Native Development Several components like tests, linting, and scripts can be run either inside of the Docker container, or outside on your native machine.