Skip to content

Commit

Permalink
[Issue 1325]: add python interactive console (#1331)
Browse files Browse the repository at this point in the history
Summary
Fixes #1325

Time to review: 5 min
add make console target that loads Python interactive console
  • Loading branch information
rylew1 authored Feb 28, 2024
1 parent 148e1ff commit 8007004
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 0 deletions.
3 changes: 3 additions & 0 deletions api/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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}'
7 changes: 7 additions & 0 deletions api/src/db/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Empty file added api/src/tool/__init__.py
Empty file.
Empty file.
132 changes: 132 additions & 0 deletions api/src/tool/console/interactive.py
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

# 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)
4 changes: 4 additions & 0 deletions documentation/api/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 8007004

Please sign in to comment.