From a0da4d4641fef79cc63aac43fd5870eab41cfdda Mon Sep 17 00:00:00 2001 From: Iain Learmonth Date: Wed, 15 Jun 2022 11:50:15 +0100 Subject: [PATCH] brn: Introduce BRN as a class --- .pylintrc | 5 ++++ app/brm/__init__.py | 3 +++ app/brm/brn.py | 58 ++++++++++++++++++++++++++++++++++++++++++ app/brm/utils.py | 20 +++++++++++++++ app/models/__init__.py | 3 ++- app/models/mirrors.py | 21 ++++++++++++--- app/models/onions.py | 13 +++++++--- requirements.txt | 2 ++ 8 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 .pylintrc create mode 100644 app/brm/__init__.py create mode 100644 app/brm/brn.py create mode 100644 app/brm/utils.py diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..3816d2e --- /dev/null +++ b/.pylintrc @@ -0,0 +1,5 @@ +[MASTER] +ignored-classes=Column +load-plugins=pylint_flask,pylint_flask_sqlalchemy +py-version=3.8 +suggestion-mode=yes diff --git a/app/brm/__init__.py b/app/brm/__init__.py new file mode 100644 index 0000000..c850fc8 --- /dev/null +++ b/app/brm/__init__.py @@ -0,0 +1,3 @@ +""" +Bypass Censorship Resource Management API. +""" diff --git a/app/brm/brn.py b/app/brm/brn.py new file mode 100644 index 0000000..5e7b998 --- /dev/null +++ b/app/brm/brn.py @@ -0,0 +1,58 @@ +""" +Bypass Censorship Resource Names. +""" + +from __future__ import annotations +from dataclasses import dataclass, field +from typing import Any + +from flask import current_app + +from app.brm.utils import is_integer + + +def global_namespace() -> str: + return str(current_app.config["GLOBAL_NAMESPACE"]) + + +@dataclass +class BRN: + group_id: int + product: str + provider: str + resource_type: str + resource_id: str + global_namespace: str = field(default_factory=global_namespace) + + @classmethod + def from_str(cls, s: str) -> BRN: + parts = s.split(":") + if len(parts) != 6 or parts[0].lower() != "brn" or not is_integer(parts[2]): + raise TypeError(f"Expected a valid BRN but got {repr(s)}.") + resource_parts = parts[4].split("/") + if len(resource_parts) != 5: + raise TypeError(f"Expected a valid BRN but got {repr(s)}.") + return cls( + global_namespace=parts[1], + group_id=int(parts[2]), + product=parts[3], + provider=parts[4], + resource_type=resource_parts[0], + resource_id=resource_parts[1] + ) + + def __eq__(self, other: Any) -> bool: + return str(self) == str(other) + + def __str__(self) -> str: + return ":".join([ + "brn", + self.global_namespace, + str(self.group_id), + self.product, + self.provider, + f"{self.resource_type}/{self.resource_id}" + ]) + + def __repr__(self) -> str: + return f"" diff --git a/app/brm/utils.py b/app/brm/utils.py new file mode 100644 index 0000000..90f187e --- /dev/null +++ b/app/brm/utils.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from typing import Any + + +def is_integer(n: Any) -> bool: + """ + Determine if a string (or other object type that can be converted automatically) represents an integer. + + Thanks to https://note.nkmk.me/en/python-check-int-float/. + + :param n: object to test + :return: true if it's an integer + """ + try: + float(n) + except ValueError: + return False + else: + return float(n).is_integer() diff --git a/app/models/__init__.py b/app/models/__init__.py index 0ad9ca9..03fcec3 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -3,6 +3,7 @@ from datetime import datetime from typing import Union, List, Optional, Any from app.alarms import alarms_for +from app.brm.brn import BRN from app.extensions import db from app.models.alarms import Alarm @@ -73,7 +74,7 @@ class AbstractResource(db.Model): # type: ignore @property @abstractmethod - def brn(self) -> str: + def brn(self) -> BRN: raise NotImplementedError() def deprecate(self, *, reason: str) -> None: diff --git a/app/models/mirrors.py b/app/models/mirrors.py index 1cfee70..b2923f6 100644 --- a/app/models/mirrors.py +++ b/app/models/mirrors.py @@ -3,6 +3,7 @@ from typing import Optional, List from flask import current_app from tldextract import extract +from app.brm.brn import BRN from app.extensions import db from app.models import AbstractConfiguration, AbstractResource from app.models.onions import Onion @@ -53,8 +54,14 @@ class Proxy(AbstractResource): origin = db.relationship("Origin", back_populates="proxies") @property - def brn(self) -> str: - return f"brn:{current_app.config['GLOBAL_NAMESPACE']}:{self.origin.group_id}:mirror:{self.provider}:proxy/{self.id}" + def brn(self) -> BRN: + return BRN( + group_id=self.origin.group_id, + product="mirror", + provider=self.provider, + resource_type="proxy", + resource_id=self.id + ) @classmethod def csv_header(cls) -> List[str]: @@ -72,5 +79,11 @@ class SmartProxy(AbstractResource): group = db.relationship("Group", back_populates="smart_proxies") @property - def brn(self) -> str: - return f"brn:{current_app.config['GLOBAL_NAMESPACE']}:{self.group_id}:mirror:{self.provider}:smart-proxy/1" + def brn(self) -> BRN: + return BRN( + group_id=self.group_id, + product="mirror", + provider=self.provider, + resource_type="smart_proxy", + resource_id=str(1) + ) diff --git a/app/models/onions.py b/app/models/onions.py index 679bd1d..6f6bd33 100644 --- a/app/models/onions.py +++ b/app/models/onions.py @@ -1,5 +1,4 @@ -from flask import current_app - +from app.brm.brn import BRN from app.extensions import db from app.models import AbstractConfiguration, AbstractResource @@ -21,5 +20,11 @@ class Eotk(AbstractResource): group = db.relationship("Group", back_populates="eotks") @property - def brn(self) -> str: - return f"brn:{current_app.config['GLOBAL_NAMESPACE']}:{self.group_id}:eotk:{self.provider}:instance/{self.region}" + def brn(self) -> BRN: + return BRN( + group_id=self.group_id, + provider=self.provider, + product="eotk", + resource_type="instance", + resource_id=self.region + ) diff --git a/requirements.txt b/requirements.txt index b6269c7..f90e365 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,8 @@ flask-wtf flask~=2.0.2 jinja2~=3.0.2 pydantic +pylint-flask-sqlalchemy +pylint-flask pyyaml~=6.0 requests~=2.27.1 sqlalchemy~=1.4.32