Source code for aequilibrae.project.project

import functools
import logging
import os
import shutil
import sqlite3
from contextlib import contextmanager
from collections import namedtuple
from pathlib import Path

from aequilibrae import global_logger
from aequilibrae.context import activate_project, get_active_project
from aequilibrae.log import Log
from aequilibrae.log import get_log_handler
from aequilibrae.parameters import Parameters
from aequilibrae.project.about import About
from aequilibrae.project.data import Matrices
from aequilibrae.project.network import Network
from aequilibrae.project.project_cleaning import clean
from aequilibrae.project.project_creation import initialize_tables
from aequilibrae.project.zoning import Zoning
from aequilibrae.project.tools import MigrationManager
from aequilibrae.project.database_connection import database_connection
from aequilibrae.reference_files import spatialite_database, demo_init_py
from aequilibrae.transit.transit import Transit
from aequilibrae.utils.db_utils import commit_and_close
from aequilibrae.utils.model_run_utils import import_file_as_module


[docs] class Project: """AequilibraE project class .. code-block:: python :caption: Create Project >>> new_project = Project() >>> new_project.new(project_path) .. code-block:: python :caption: Open Project >>> existing_project = Project() >>> existing_project.open(project_path) """
[docs] def __init__(self): self.path_to_file: str = None self.project_base_path = Path() self.source: str = None self.network: Network = None self.about: About = None self.logger: logging.Logger = None self.transit: Transit = None
[docs] @classmethod def from_path(cls, project_folder): project = Project() project.open(project_folder) return project
[docs] def open(self, project_path: str) -> None: """ Loads project from disk :Arguments: **project_path** (:obj:`str`): Full path to the project data folder. If the project inside does not exist, it will fail. """ self.project_base_path = Path(project_path) file_name = self.project_base_path / "project_database.sqlite" if not file_name.is_file() or not file_name.exists(): raise FileNotFoundError("Model does not exist. Check your path and try again") self.path_to_file = file_name self.source = self.path_to_file self.__setup_logger() self.activate() self.__load_objects() global_logger.info(f"Opened project on {self.project_base_path}") clean(self)
@property @contextmanager def db_connection(self): with commit_and_close(self.path_to_file, spatial=True) as conn: yield conn
[docs] def new(self, project_path: str) -> None: """Creates a new project :Arguments: **project_path** (:obj:`str`): Full path to the project data folder. If folder exists, it will fail """ self.project_base_path = Path(project_path) self.path_to_file = self.project_base_path / "project_database.sqlite" self.source = self.path_to_file if os.path.isdir(project_path): raise FileExistsError("Location already exists. Choose a different name or remove the existing directory") # We create the project folder and create the base file self.project_base_path.mkdir(parents=True, exist_ok=True) self.__setup_logger() self.activate() self.__create_empty_network() self.__load_objects() self.about.create() global_logger.info(f"Created project on {self.project_base_path}")
[docs] def close(self) -> None: """Safely closes the project""" if not self.project_base_path: global_logger.warning("This Aequilibrae project is not opened") return try: with self.project.db_connection as conn: conn.commit() clean(self) for obj in [self.parameters, self.network]: del obj del self.network.link_types del self.network.modes global_logger.info(f"Closed project on {self.project_base_path}") except (sqlite3.ProgrammingError, AttributeError): global_logger.warning(f"This project at {self.project_base_path} is already closed") finally: self.deactivate()
[docs] def activate(self): activate_project(self)
[docs] def deactivate(self): if get_active_project(must_exist=False) is self: activate_project(None)
[docs] def log(self) -> Log: """Returns a log object allows the user to read the log or clear it""" return Log(self.project_base_path)
[docs] def upgrade(self): """ Find and apply all applicable migrations. All database upgrades are applied within a single transaction. If skipping a specific migration is required, use the ``aequilbrae.project.tools.MigrationManager`` object directly. Consult it's documentation page for details. Take care when skipping migrations. """ global_logger.info("Starting database upgrades") targets = [ (MigrationManager(MigrationManager.network_migration_file), database_connection("project")), ] if (self.project_base_path / "public_transport.sqlite").exists(): targets.append((MigrationManager(MigrationManager.transit_migration_file), database_connection("transit"))) try: for mm, conn in targets: with conn: mm.mark_all_as_seen(conn) for mm, conn in targets: with conn: mm.upgrade(conn) global_logger.info("Completed database upgrades") finally: for _, conn in targets: conn.close()
def __load_objects(self): matrix_folder = self.project_base_path / "matrices" matrix_folder.mkdir(parents=True, exist_ok=True) self.network = Network(self) self.about = About(self) self.matrices = Matrices(self) @property def project_parameters(self) -> Parameters: return Parameters(self) @property def parameters(self) -> dict: return self.project_parameters.parameters @property def run(self): """ Load and return the AequilibraE run module with the default arguments from ``parameters.yml`` partially applied. Refer to ``run/__init__.py`` file within the project folder for documentation. """ entry_points = self.parameters["run"] module = import_file_as_module(self.project_base_path / "run" / "__init__.py", "aequilibrae.run", force=True) res = [] sentinal = object() for name, kwargs in entry_points.items(): attr = getattr(module, name) if attr is sentinal: raise RuntimeError(f"expected to find callable '{name}' in the run module but didn't") elif not callable(attr): raise RuntimeError(f"found symbol '{name}' in the run module but it is not callable") func = functools.partial(attr, **(kwargs if kwargs is not None else {})) res.append((name, func)) Run = namedtuple("Run", [k for k, _ in res]) return Run._make([v for _, v in res])
[docs] def check_file_indices(self) -> None: """Makes results_database.sqlite and the matrices folder compatible with project database""" raise NotImplementedError
@property def zoning(self): return Zoning(self.network) def __create_empty_network(self): shutil.copyfile(spatialite_database, self.path_to_file) pth = self.project_base_path / "run" pth.mkdir(parents=True, exist_ok=True) shutil.copyfile(demo_init_py, pth / "__init__.py") # Write parameters to the project folder p = self.project_parameters p.parameters["system"]["logging_directory"] = str(self.project_base_path) p.write_back() # Create actual tables with self.db_connection as conn: conn.execute("PRAGMA foreign_keys = ON;") initialize_tables(self, "network") def __setup_logger(self): self.logger = logging.getLogger(f"aequilibrae.{self.project_base_path}") self.logger.propagate = False self.logger.setLevel(logging.DEBUG) par = self.parameters or self.project_parameters._default do_log = par["system"]["logging"] if do_log: self.logger.addHandler(get_log_handler(self.project_base_path / "aequilibrae.log"))