Skip to content

Commit

Permalink
cleaning up more stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
plockaby committed Feb 24, 2024
1 parent c5bcf3a commit 918a5af
Show file tree
Hide file tree
Showing 11 changed files with 100 additions and 73 deletions.
2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 38 additions & 16 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,62 @@ exclude: '^$'
fail_fast: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v4.5.0
hooks:
- id: check-ast
- id: check-added-large-files
- id: check-json
- id: check-yaml
- id: check-shebang-scripts-are-executable
- id: check-symlinks
- id: destroyed-symlinks
- id: check-merge-conflict
- id: check-case-conflict
- id: mixed-line-ending
exclude: "(^.idea/|.vscode/)"
- id: trailing-whitespace
exclude: "(^.idea/|^docs/)"
exclude: "(^.idea/|.vscode/)"
- id: end-of-file-fixer
exclude: "(^.idea/|^docs/)"
exclude: "(^.idea/|.vscode/)"

- repo: https://github.com/pycqa/isort
rev: 5.12.0
# this checks whether the python code is valid
- id: check-ast

- repo: https://github.com/google/yapf
rev: v0.40.2
hooks:
- id: isort
- id: yapf
exclude: ^.*\b(migrations)\b.*$

- repo: https://github.com/psf/black
rev: 23.1.0
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: black
- id: isort

- repo: https://github.com/pycqa/flake8
rev: 5.0.4
rev: 7.0.0
hooks:
- id: flake8
additional_dependencies:
- flake8-annotations
# automated security testing
- flake8-bandit
- flake8-black

# do not allow breaking lines with backslashes
- flake8-broken-line

# check for built-ins being used as variables or parameters
- flake8-builtins

# "find likely bugs and design problems in your program"
- flake8-bugbear
- flake8-commas

# write better comprehensions
- flake8-comprehensions
- flake8-isort

# catch bugs from implicit concat
- flake8-no-implicit-concat
- flake8-quotes

# attempt to simplify code
- flake8-simplify

# validate names of things
- pep8-naming
3 changes: 3 additions & 0 deletions .style.yapf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[style]
COLUMN_LIMIT=120
ALLOW_SPLIT_BEFORE_DICT_VALUE=false
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ RUN poetry config virtualenvs.in-project true && \

# now copy over the application
COPY --chown=1000:1000 src/$APP_NAME $APP_ROOT/$APP_NAME
COPY --chown=1000:1000 src/configurations $APP_ROOT/configurations

# update the version number of our application
COPY --chown=1000:1000 .git/ $APP_ROOT/.git
Expand All @@ -46,6 +47,7 @@ FROM base AS final
COPY --from=builder --chown=0:0 $APP_ROOT/entrypoint /entrypoint
COPY --from=builder --chown=0:0 $APP_ROOT/.venv $APP_ROOT/.venv
COPY --from=builder --chown=0:0 $APP_ROOT/$APP_NAME $APP_ROOT/$APP_NAME
COPY --from=builder --chown=0:0 $APP_ROOT/configurations $APP_ROOT/configurations

# set up the virtual environment
ENV VIRTUALENV=$APP_ROOT/.venv
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ poetry run python3 -m enphase_proxy
Still assuming that your environment is configured, an alternative way to run this is with `hypercorn`, like this:

```
cd src/
poetry run hypercorn \
--bind=127.0.0.1:8080 \
--access-logfile=- \
Expand Down
4 changes: 2 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ packages = [{include = "enphase_proxy", from = "src"}]
[tool.poetry.scripts]

[tool.poetry.dependencies]
python = "^3.11"
python = "^3.12"
quart = "^0.19.3"
aiohttp = "^3.8.6"
tenacity = "^8.2.3"
Expand Down
23 changes: 22 additions & 1 deletion src/enphase_proxy/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@
default=False,
help="send verbose output to the console",
)
parser.add_argument(
"-p",
"--port",
dest="port",
default=8080,
type=int,
help="port number to listen on",
)
parser.add_argument(
"-b",
"--bind",
dest="address",
default="127.0.0.1",
type=str,
help="interface to listen on",
)
args = parser.parse_args()

# send application logs to stdout
Expand All @@ -31,4 +47,9 @@
)

# run only on localhost for testing
load().run(host="127.0.0.1", port=8080, debug=args.verbose, use_reloader=False)
load().run(
host=args.bind,
port=args.port,
debug=args.verbose,
use_reloader=False,
)
27 changes: 9 additions & 18 deletions src/enphase_proxy/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,7 @@ def load() -> Quart:
app = Quart(__name__, static_folder=None)
environment = load_configuration(app, package="configurations")
app.config.from_prefixed_env("ENPHASE")
app.logger.info(
"starting web application in '{}' mode with version {}".format(
environment,
__version__,
),
)
app.logger.info("starting web application in '%s' mode with version %s", environment, __version__)

# initialize the system that fetches the enphase jwt
credentials_updater = CredentialsUpdater(app)
Expand All @@ -39,23 +34,19 @@ async def shutdown() -> None:
@app.route("/_/health")
async def health() -> Response:
return await make_response(
jsonify(
{
"status": "pass",
"message": "flux capacitor is fluxing",
"version": __version__,
},
),
200,
)
jsonify({
"status": "pass",
"message": "flux capacitor is fluxing",
"version": __version__,
}), 200)

@app.route("/", defaults={"path": ""})
@app.route("/<path:path>")
async def proxy(path: str) -> Response:
async with app.config["LOCAL_API_SESSION"].get(
f"/{path}",
ssl=False,
headers={"Authorization": f"Bearer {credentials_updater.credentials}"},
f"/{path}",
ssl=False,
headers={"Authorization": f"Bearer {credentials_updater.credentials}"},
) as result:
content = await result.text()
status_code = result.status
Expand Down
17 changes: 7 additions & 10 deletions src/enphase_proxy/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,24 @@ def load_configuration(

if path is None:
# load from a package called "{calling_package}.configurations"
calling_package = inspect.currentframe().f_back.f_globals["__package__"]
current_frame = inspect.currentframe()
calling_package = current_frame.f_back.f_globals["__package__"]
if calling_package:
package = ".".join([calling_package, "configurations"])
else:
package = "configurations"

with importlib.resources.as_file(
importlib.resources.files(package) / "{}.conf".format(environment),
) as path:
logger.info("loading configuration from '{}'".format(path))
with importlib.resources.as_file(importlib.resources.files(package) / f"{environment}.conf") as path:
logger.info("loading configuration from '%s'", path)
app.config.from_pyfile(path)
else:
path = os.path.join(path, "{}.conf".format(environment))
logger.info("loading configuration from '{}'".format(path))
logger.info("loading configuration from '%s'", path)
app.config.from_pyfile(path)

else:
with importlib.resources.as_file(
importlib.resources.files(package) / "{}.conf".format(environment),
) as path:
logger.info("loading configuration from '{}'".format(path))
with importlib.resources.as_file(importlib.resources.files(package) / f"{environment}.conf") as path:
logger.info("loading configuration from '%s'", path)
app.config.from_pyfile(path)

return environment
38 changes: 15 additions & 23 deletions src/enphase_proxy/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import contextlib
import logging
from datetime import datetime
from time import perf_counter as time_now
from typing import Optional

import aiohttp
Expand All @@ -13,6 +12,7 @@


class CredentialsUpdater:

def __init__(self: "CredentialsUpdater", app: Quart = None) -> None:
# this is how often we will refresh our credentials. we are not
# actually fetching credentials this often. this is just how often we
Expand Down Expand Up @@ -51,20 +51,18 @@ async def startup() -> None:
# we are going to start a background task that will continue to
# fetch the credentials on a regular basis. we can't start until
# we have credentials so just crash if we can't fetch them.
self.app.logger.info("fetching initial credentials")
logger.info("fetching initial credentials")
self.data_cache = await self.data_manager.credentials

self.app.logger.info("registering credentials updater background task")
logger.info("registering credentials updater background task")
app.add_background_task(self._background_looper)

@app.after_serving
async def shutdown() -> None:
# just set the Event flag and the background task will exit. if the
# background task does not exit within 60 seconds then Quart will
# send an async cancel event to the task.
self.app.logger.info(
"signaling credentials updater background task to stop",
)
logger.info("signaling credentials updater background task to stop")
self.updater_canceled.set()

async def _background_waiter(
Expand All @@ -79,16 +77,13 @@ async def _background_waiter(

async def _background_looper(self: "CredentialsUpdater") -> None:
with contextlib.suppress(asyncio.CancelledError):
while not await self._background_waiter(
self.updater_canceled,
self.updater_refresh,
):
while not await self._background_waiter(self.updater_canceled, self.updater_refresh):
# we need the background task to keep looping and try again
# so ignore any error that comes out of it and start again
with contextlib.suppress(Exception):
await self._background_task()

self.app.logger.info("credentials updater background task shutting down")
logger.info("credentials updater background task shutting down")

async def _background_task(self: "CredentialsUpdater") -> None:
# Randomly wait up to 2^x * 10 seconds between each retry, at least 60
Expand All @@ -97,30 +92,26 @@ async def _background_task(self: "CredentialsUpdater") -> None:
@tenacity.retry(
wait=tenacity.wait_random_exponential(multiplier=10, min=60, max=600),
stop=tenacity.stop_when_event_set(self.updater_canceled),
before_sleep=tenacity.before_sleep_log(self.app.logger, logging.ERROR),
before_sleep=tenacity.before_sleep_log(logger, logging.ERROR),
reraise=True,
)
async def task() -> None:
try:
self.data_cache = await self.data_manager.credentials
except Exception as e:
self.app.logger.exception(f"unable to fetch credentials: {e}")
logger.exception("unable to fetch credentials: %s", str(e))
raise

timer = time_now()
await task()
self.app.logger.info(
"finished refreshing credentials in {:.4f} seconds".format(
time_now() - timer,
),
)
logger.info("finished refreshing credentials")

@property
def credentials(self: "CredentialsUpdater") -> str:
return self.data_cache


class CredentialsManager:

def __init__(
self: "CredentialsManager",
url: Optional[str] = None,
Expand Down Expand Up @@ -158,17 +149,18 @@ async def credentials(self: "CredentialsManager") -> str:
now = datetime.now()
if (now - self.data["fetched"]) > (self.data["expiry"] - now):
logger.info(
f"credentials will expire at {self.data['expiry']} -- fetching new credentials",
"credentials will expire at %s -- fetching new credentials",
self.data["expiry"],
)
self.data = await self._fetch_credentials()

return self.data["token"]

async def _fetch_credentials(self: "CredentialsManager") -> dict:
async with aiohttp.ClientSession(
raise_for_status=True,
base_url=self.enphase_url,
skip_auto_headers={"User-Agent"},
raise_for_status=True,
base_url=self.enphase_url,
skip_auto_headers={"User-Agent"},
) as session:
# get the session id
url = "/login/login.json"
Expand Down

0 comments on commit 918a5af

Please sign in to comment.