Skip to content

Commit

Permalink
Merge pull request #272 from thegridelectric/dev
Browse files Browse the repository at this point in the history
Publish latest changes on dev
  • Loading branch information
anschweitzer authored Dec 28, 2024
2 parents 20ab1ed + ed9f582 commit 672f48b
Show file tree
Hide file tree
Showing 56 changed files with 5,335 additions and 2,792 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,14 @@ jobs:
- name: Publish package on PyPI
if: steps.check-version.outputs.tag
uses: pypa/gh-action-pypi-publish@v1.10.2
uses: pypa/gh-action-pypi-publish@v1.12.2
with:
user: __token__
password: ${{ secrets.PYPI_TOKEN }}

- name: Publish package on TestPyPI
if: "! steps.check-version.outputs.tag"
uses: pypa/gh-action-pypi-publish@v1.10.2
uses: pypa/gh-action-pypi-publish@v1.12.2
with:
user: __token__
password: ${{ secrets.TEST_PYPI_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -238,4 +238,4 @@ jobs:
nox --session=coverage -- xml
- name: Upload coverage report
uses: codecov/codecov-action@v4.5.0
uses: codecov/codecov-action@v5.1.2
2,017 changes: 1,044 additions & 973 deletions poetry.lock

Large diffs are not rendered by default.

31 changes: 18 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "gridworks-proactor"
version = "1.0.0"
version = "1.1.0"
description = "Gridworks Proactor"
authors = ["Jessica Millar <[email protected]>"]
license = "MIT"
Expand All @@ -16,6 +16,9 @@ classifiers = [
"Development Status :: 3 - Alpha",
]

[tool.poetry.scripts]
gwtest = "gwproactor_test.cli:app"

[tool.poetry.urls]
Changelog = "https://github.com/thegridelectric/gridworks-proactor/releases"

Expand All @@ -26,21 +29,21 @@ python-dotenv = "^1.0.0"
xdg = "^6.0.0"
paho-mqtt = "^1.6.1"
result = "^0.9.0"
gridworks-protocol = "^1.0.2"
#gridworks-protocol = {path="../gridworks-protocol", develop=true}
#gridworks-protocol = {git = "https://github.com/thegridelectric/gridworks-protocol.git", branch="jm/report"}
gridworks-protocol = "^1.2.0"
#gridworks-protocol = { path = "../gridworks-protocol", develop = true }
#gridworks-protocol = { git = "https://github.com/thegridelectric/gridworks-protocol.git", branch = "dev" }
aiohttp = "^3.8.5"
yarl = "^1.9.2"
multidict = "^6.0.4"
pytest = {version = ">=7.2.0", optional = true}
pytest-asyncio = {version = ">=0.20.3", optional = true}
gridworks-cert = {version = ">=0.4.2", optional = true}
freezegun = {version = "^1.5.1", optional = true}
pytest = { version = ">=7.2.0", optional = true }
pytest-asyncio = { version = ">=0.20.3", optional = true }
gridworks-cert = { version = ">=0.4.2", optional = true }
freezegun = { version = "^1.5.1", optional = true }
pydantic-settings = "^2.4.0"

[tool.poetry.group.dev.dependencies]
Pygments = ">=2.10.0"
coverage = {extras = ["toml"], version = ">=6.2"}
coverage = { extras = ["toml"], version = ">=6.2" }
furo = ">=2021.11.12"
mypy = ">=0.930"
pep8-naming = ">=0.12.1"
Expand All @@ -50,13 +53,13 @@ pyupgrade = ">=2.29.1"
sphinx = ">=4.3.2"
sphinx-autobuild = ">=2021.3.14"
sphinx-click = ">=3.0.2"
xdoctest = {extras = ["colors"], version = ">=0.15.10"}
myst-parser = {version = ">=0.16.1"}
xdoctest = { extras = ["colors"], version = ">=0.15.10" }
myst-parser = { version = ">=0.16.1" }
sphinxcontrib-mermaid = "^0.8.1"
ruff = "^0.6.2"

[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope="function"
asyncio_default_fixture_loop_scope = "function"

[tool.poetry.extras]
tests = ["pytest", "pytest-asyncio", "gridworks-cert", "freezegun"]
Expand All @@ -71,7 +74,7 @@ source = ["gwproactor", "gwproactor_test", "tests"]

[tool.coverage.report]
show_missing = false
fail_under = 80
fail_under = 75

[tool.mypy]
strict = true
Expand Down Expand Up @@ -164,6 +167,7 @@ ignore = [
"C901", # Complexity
"FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize()
"G004", # Ok to use f-strings in logging in test code
"N806", # We want to be able to have non pep8 variable names.
"PLR0912", # Complexity
"PLR0913", # Complexity
"PLR0915", # Complexity
Expand All @@ -175,6 +179,7 @@ ignore = [
"C901", # Complexity
"FBT",
"G004",
"N806", # We want to be able to have non pep8 variable names.
"PLR0912",
"PLR0913",
"PLR0915",
Expand Down
7 changes: 4 additions & 3 deletions src/gwproactor/actors/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from abc import ABC
from typing import Any, Generic, Sequence, TypeVar

from gwproto import Message, ShNode
from gwproto import Message
from gwproto.data_classes.sh_node import ShNode
from result import Result

from gwproactor.proactor_interface import (
Expand All @@ -20,8 +21,6 @@


class Actor(ActorInterface, Communicator, ABC):
_node: ShNode

def __init__(self, name: str, services: ServicesInterface) -> None:
self._node = services.hardware_layout.node(name)
super().__init__(name, services)
Expand All @@ -31,6 +30,8 @@ def name(self) -> str:
return self._name

@property
# note this is over-written in the derived class ScadaActor
# so that it is not static.
def node(self) -> ShNode:
return self._node

Expand Down
183 changes: 183 additions & 0 deletions src/gwproactor/command_line_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import argparse
import logging
import sys
import traceback
from pathlib import Path
from typing import Optional, Type, TypeVar

import dotenv
import rich
from pydantic import BaseModel
from pydantic_settings import BaseSettings

from gwproactor import Proactor, ProactorSettings, setup_logging
from gwproactor.config import MQTTClient
from gwproactor.config.paths import TLSPaths

LOGGING_FORMAT = "%(asctime)s %(message)s"


def missing_tls_paths(paths: TLSPaths) -> list[tuple[str, Optional[Path]]]:
missing = []
for path_name in paths.model_fields:
path = getattr(paths, path_name)
if path is None or not Path(path).exists():
missing.append((path_name, path))
return missing


def check_tls_paths_present(
model: BaseModel | BaseSettings, *, raise_error: bool = True
) -> str:
missing_str = ""
for k in model.model_fields:
v = getattr(model, k)
if isinstance(v, MQTTClient) and v.tls.use_tls:
missing_paths = missing_tls_paths(v.tls.paths)
if missing_paths:
missing_str += f"client {k}\n"
for path_name, path in missing_paths:
missing_str += f" {path_name:20s} {path}\n"
if missing_str:
error_str = f"ERROR. TLS usage requested but the following files are missing:\n{missing_str}"
if raise_error:
raise ValueError(error_str)
else:
error_str = ""
return error_str


ProactorT = TypeVar("ProactorT", bound=Proactor)
ProactorSettingsT = TypeVar("ProactorSettingsT", bound=ProactorSettings)


def get_settings(
*,
settings_type: Optional[Type[ProactorSettingsT]] = None,
settings: Optional[ProactorSettingsT] = None,
env_file: str | Path = ".env",
) -> ProactorSettingsT:
if (settings_type is None and settings is None) or (
settings_type is not None and settings is not None
):
raise ValueError("ERROR. Specify exactly one of (settings_type, settings)")
if settings_type is not None:
settings = settings_type(_env_file=str(env_file))
return settings


def print_settings(
*,
settings_type: Optional[Type[ProactorSettingsT]] = None,
settings: Optional[ProactorSettingsT] = None,
env_file: str | Path = ".env",
) -> None:
dotenv_file = dotenv.find_dotenv(env_file)
rich.print(
f"Env file: <{dotenv_file}> exists:{env_file and Path(dotenv_file).exists()}"
)
settings = get_settings(
settings_type=settings_type, settings=settings, env_file=dotenv_file
)
rich.print(settings)
missing_tls_paths_ = check_tls_paths_present(settings, raise_error=False)
if missing_tls_paths_:
rich.print(missing_tls_paths_)


def get_proactor( # noqa: PLR0913
name: str,
proactor_type: Type[ProactorT],
*,
settings_type: Optional[Type[ProactorSettingsT]] = None,
settings: Optional[ProactorSettingsT] = None,
dry_run: bool = False,
verbose: bool = False,
message_summary: bool = False,
env_file: str | Path = ".env",
run_in_thread: bool = False,
add_screen_handler: bool = True,
) -> ProactorT:
dotenv_file = dotenv.find_dotenv(env_file)
dotenv_file_debug_str = (
f"Env file: <{dotenv_file}> exists:{Path(dotenv_file).exists()}"
)
settings = get_settings(
settings_type=settings_type, settings=settings, env_file=dotenv_file
)
if dry_run:
rich.print(dotenv_file_debug_str)
rich.print(settings)
missing_tls_paths_ = check_tls_paths_present(settings, raise_error=False)
if missing_tls_paths_:
rich.print(missing_tls_paths_)
rich.print("Dry run. Doing nothing.")
sys.exit(0)
else:
settings.paths.mkdirs()
args = argparse.Namespace(
verbose=verbose,
message_summary=message_summary,
)
setup_logging(args, settings, add_screen_handler=add_screen_handler)
logger = logging.getLogger(
settings.logging.qualified_logger_names()["lifecycle"]
)
logger.info("")
logger.info(dotenv_file_debug_str)
logger.info("Settings:")
logger.info(settings.model_dump_json(indent=2))
rich.print(settings)
check_tls_paths_present(settings)
proactor = proactor_type(name=name, settings=settings)
if run_in_thread:
logger.info("run_async_actors_main() starting")
proactor.run_in_thread()
return proactor


async def run_async_main( # noqa: PLR0913
name: str,
proactor_type: Type[ProactorT],
*,
settings_type: Optional[Type[ProactorSettingsT]] = None,
settings: Optional[ProactorSettingsT] = None,
env_file: str | Path = ".env",
dry_run: bool = False,
verbose: bool = False,
message_summary: bool = False,
) -> None:
settings = get_settings(
settings_type=settings_type,
settings=settings,
env_file=dotenv.find_dotenv(env_file),
)
exception_logger = logging.getLogger(settings.logging.base_log_name)
try:
proactor = get_proactor(
name=name,
proactor_type=proactor_type,
settings=settings,
dry_run=dry_run,
verbose=verbose,
message_summary=message_summary,
)
exception_logger = proactor.logger
try:
await proactor.run_forever()
finally:
proactor.stop()
except SystemExit:
pass
except KeyboardInterrupt:
pass
except BaseException as e:
try:
exception_logger.exception(
"ERROR in run_async_actors_main. Shutting down: " "[%s] / [%s]",
e, # noqa: TRY401
type(e), # noqa: TRY401
)
except: # noqa: E722
traceback.print_exception(e)
raise
Loading

0 comments on commit 672f48b

Please sign in to comment.