Minor fixes

Signed-off-by: András Schmelczer <andras@schmelczer.dev>
This commit is contained in:
Andras Schmelczer 2022-04-10 12:59:05 +02:00
parent d488627376
commit 4b72bddeb9
17 changed files with 73 additions and 60 deletions

View file

@ -6,6 +6,7 @@
"inplace", "inplace",
"levelno", "levelno",
"plotly", "plotly",
"psutil",
"pydantic", "pydantic",
"pyplot", "pyplot",
"sklearn", "sklearn",

4
.vscode/tasks.json vendored
View file

@ -4,9 +4,9 @@
{ {
"label": "Format and lint Python", "label": "Format and lint Python",
"type": "shell", "type": "shell",
"command": "source .env/bin/activate && scripts/format-python.sh good_ai && scripts/check-python.sh good_ai", "command": "source .env/bin/activate && scripts/format-python.sh good_ai && scripts/format-python.sh example",
"windows": { "windows": {
"command": ".env/bin/activate.bat; scripts/format-python.sh good_ai; scripts\\check-python.sh good_ai" "command": ".env/bin/activate.bat; scripts/format-python.sh good_ai; scripts\\format-python.sh example"
}, },
"group": "test", "group": "test",
"presentation": { "presentation": {

View file

@ -11,13 +11,13 @@ from good_ai import use_model, log_argument, log_metric
from good_ai.utilities.clean import clean from good_ai.utilities.clean import clean
@log_metric('text_length', calculate=lambda text: len(text))
@log_argument('text', expected_type=str, validator=lambda t: len(t) > 0)
@use_model(model_key, version="latest") @use_model(model_key, version="latest")
@log_argument('text', validator=lambda t: len(t) > 0)
def predict_domain( def predict_domain(
text: str, model: Pipeline, cut_off_probability: float = 0.2 text: str, model: Pipeline, cut_off_probability: float = 0.2
) -> List[DomainPrediction]: ) -> List[DomainPrediction]:
assert 0 <= cut_off_probability <= 1 assert 0 <= cut_off_probability <= 1
log_metric('text_length', len(text))
cleaned = clean(text, convert_to_ascii=True) cleaned = clean(text, convert_to_ascii=True)
text = re.sub(r"[^a-zA-Z0-9]", " ", cleaned) text = re.sub(r"[^a-zA-Z0-9]", " ", cleaned)

View file

@ -9,7 +9,6 @@ class Context(BaseModel):
metrics_path: str metrics_path: str
persistence: PersistenceDriver persistence: PersistenceDriver
is_production: bool is_production: bool
is_threadsafe: bool
logger: Logger logger: Logger
class Config: class Config:

View file

@ -7,7 +7,7 @@ from typing import Optional, cast
from good_ai.open_s3 import LargeFile from good_ai.open_s3 import LargeFile
from good_ai.utilities.logger import create_logger from good_ai.utilities.logger import create_logger
from ..persistence import PersistenceDriver, TinyDbDriver from ..persistence import PersistenceDriver, TinyDbDriver, ParallelTinyDbDriver
from .context import Context from .context import Context
_context: Optional[Context] = None _context: Optional[Context] = None
@ -25,7 +25,7 @@ def set_default_config(
log_level: int = INFO, log_level: int = INFO,
s3_config: Path = Path("s3.ini"), s3_config: Path = Path("s3.ini"),
seed: int = 42, seed: int = 42,
persistence_driver: PersistenceDriver = TinyDbDriver(Path("tracing_database.json")), persistence_driver: PersistenceDriver = ParallelTinyDbDriver(Path("tracing_database.json")),
is_production_mode_override: Optional[bool] = None, is_production_mode_override: Optional[bool] = None,
) -> None: ) -> None:
global _context global _context
@ -38,14 +38,12 @@ def set_default_config(
_initialize_large_file(s3_config, logger=logger) _initialize_large_file(s3_config, logger=logger)
_set_seed(seed) _set_seed(seed)
is_threadsafe = not isinstance(persistence_driver, TinyDbDriver) if not persistence_driver.is_threadsafe:
if not is_threadsafe: logger.warn(f"The selected persistence driver ({type(persistence_driver).__name__}) is not threadsafe")
logger.warn("The selected persistence driver (TinyDbDriver) is not threadsafe")
_context = Context( _context = Context(
metrics_path="/metrics", metrics_path="/metrics",
persistence=persistence_driver, persistence=persistence_driver,
is_production=is_production, is_production=is_production,
is_threadsafe=is_threadsafe,
logger=logger, logger=logger,
) )

View file

@ -18,7 +18,7 @@ def process_batch(
output = t.log_output(result) output = t.log_output(result)
return output return output
if not get_context().is_threadsafe: if not get_context().persistence.is_threadsafe:
concurrency = 1 concurrency = 1
get_context().logger.warn("Concurrency is ignored") get_context().logger.warn("Concurrency is ignored")

View file

@ -1,3 +1,2 @@
from .filter_args import filter_args
from .get_args import get_args from .get_args import get_args
from .snake_case_to_text import snake_case_to_text from .snake_case_to_text import snake_case_to_text

View file

@ -1,17 +0,0 @@
import inspect
from typing import Any, Callable, Dict
def filter_args(
dict_to_filter: Dict[str, Any], func: Callable[..., Any]
) -> Dict[str, Any]:
signature = inspect.signature(func)
filter_keys = [
param.name
for param in signature.parameters.values()
if param.kind == param.POSITIONAL_OR_KEYWORD
]
filtered_dict = {
filter_key: dict_to_filter[filter_key] for filter_key in filter_keys
}
return filtered_dict

View file

@ -1,5 +1,5 @@
from functools import wraps from functools import wraps
from typing import Any, Callable, Dict, List, Optional, Type from typing import Any, Callable, Dict, List
from ..exceptions import ArgumentValidationError from ..exceptions import ArgumentValidationError
from ..helper import get_args from ..helper import get_args
@ -9,20 +9,26 @@ from ..tracing import TracingContext
def log_argument( def log_argument(
argument_name: str, argument_name: str,
*, *,
expected_type: Optional[Type] = None,
validator: Callable[[Any], bool] = lambda _: True, validator: Callable[[Any], bool] = lambda _: True,
) -> Callable[..., Any]: ) -> Callable[..., Any]:
def decorator(func: Callable[..., Any]) -> Callable[..., Any]: def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
actual_name = f"{func.__name__}:{argument_name}" actual_name = f"arg:{func.__name__}:{argument_name}"
@wraps(func) @wraps(func)
def wrapper(*args: List[Any], **kwargs: Dict[str, Any]) -> Any: def wrapper(*args: List[Any], **kwargs: Dict[str, Any]) -> Any:
arguments = get_args(func, args, kwargs) arguments = get_args(func, args, kwargs)
argument = arguments[argument_name] argument = arguments[argument_name]
expected_type = func.__annotations__.get(argument_name)
if ( if (
expected_type is not None and not isinstance(argument, expected_type) expected_type is not None and not isinstance(argument, expected_type)
) or not validator(argument): ):
raise ArgumentValidationError(
f"Argument {argument_name} in {func.__name__} has the wrong type, expected: {expected_type.__name__}, got: {type(argument).__name__}"
)
if not validator(argument):
raise ArgumentValidationError( raise ArgumentValidationError(
f"Argument {argument_name} in {func.__name__} did not pass validation" f"Argument {argument_name} in {func.__name__} did not pass validation"
) )

View file

@ -1,26 +1,12 @@
from functools import wraps import inspect
from typing import Any, Callable, Dict, List from typing import Any
from ..helper import filter_args, get_args
from ..tracing import TracingContext from ..tracing import TracingContext
def log_metric( def log_metric(argument_name: str, value: Any) -> None:
argument_name: str, *, calculate: Callable[[Any], bool] = lambda _: True
) -> Callable[..., Any]:
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
actual_name = f"{func.__name__}:{argument_name}"
@wraps(func)
def wrapper(*args: List[Any], **kwargs: Dict[str, Any]) -> Any:
arguments = get_args(func, args, kwargs)
metric = calculate(**filter_args(arguments, calculate))
context = TracingContext.get_current_context() context = TracingContext.get_current_context()
caller = inspect.stack()[1].function
actual_name = f"metric:{caller}:{argument_name}"
if context: if context:
context.log_value(name=actual_name, value=metric) context.log_value(name=actual_name, value=value)
return func(*args, **kwargs)
return wrapper
return decorator

View file

@ -1,3 +1,4 @@
from .mongodb_driver import MongoDbDriver from .mongodb_driver import MongoDbDriver
from .persistence_driver import PersistenceDriver from .persistence_driver import PersistenceDriver
from .tinydb_driver import TinyDbDriver from .tinydb_driver import TinyDbDriver
from .parallel_tinydb_driver import ParallelTinyDbDriver

View file

@ -0,0 +1,30 @@
from pathlib import Path
from typing import Any, Callable
from black import List
from tinydb import TinyDB
from multiprocessing import Process, Lock
from ..views import Trace
from .persistence_driver import PersistenceDriver
lock = Lock()
class ParallelTinyDbDriver(PersistenceDriver):
is_threadsafe = True
def __init__(self, path_to_db: Path) -> None:
super().__init__()
self._path_to_db = path_to_db
def save_document(self, trace: Trace) -> str:
return self._safe_execute(lambda db: db.insert(trace.dict()))
def get_documents(self) -> List[Trace]:
return self._safe_execute(lambda db: [Trace.parse_obj(t) for t in db.all()])
def _safe_execute(self, func: Callable[[TinyDB], Any]) -> Any:
with lock:
with TinyDB(self._path_to_db) as db:
return func(db)

View file

@ -6,6 +6,8 @@ from good_ai.good_ai.views.trace import Trace
class PersistenceDriver(ABC): class PersistenceDriver(ABC):
is_threadsafe: bool
@abstractmethod @abstractmethod
def save_document(self, document: Trace) -> str: def save_document(self, document: Trace) -> str:
pass pass

View file

@ -10,12 +10,14 @@ from .persistence_driver import PersistenceDriver
class TinyDbDriver(PersistenceDriver): class TinyDbDriver(PersistenceDriver):
is_threadsafe = False
def __init__(self, path_to_db: Path) -> None: def __init__(self, path_to_db: Path) -> None:
super().__init__() super().__init__()
self._db = TinyDB(path_to_db) self._db = TinyDB(path_to_db)
def save_document(self, trace: Trace) -> str: def save_document(self, trace: Trace) -> str:
return self._db.insert(Document(trace.dict(), doc_id=uuid4().int)) return self._db.insert(trace.dict())
def get_documents(self) -> List[Trace]: def get_documents(self) -> List[Trace]:
return [Trace.parse_obj(t) for t in self._db.all()] return [Trace.parse_obj(t) for t in self._db.all()]

View file

@ -63,4 +63,4 @@ class TracingContext:
else: else:
get_context().logger.exception(f"Could not finish operation: {exception}") get_context().logger.exception(f"Could not finish operation: {exception}")
return True return False

View file

@ -15,7 +15,7 @@ class CustomFormatter(logging.Formatter):
logging.CRITICAL: BOLD_RED + self.fmt + RESET, logging.CRITICAL: BOLD_RED + self.fmt + RESET,
} }
def format(self, record: logging.LogRecord) -> None: def format(self, record: logging.LogRecord) -> str:
log_fmt = self.FORMATS.get(record.levelno) log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt) formatter = logging.Formatter(log_fmt)
return formatter.format(record) return formatter.format(record)

View file

@ -4,6 +4,9 @@ from typing import Any, Callable, Iterable, List, Optional
import multiprocess as mp import multiprocess as mp
import psutil import psutil
from tqdm.auto import tqdm from tqdm.auto import tqdm
from .logger import create_logger
logger = create_logger('parallel_map')
def parallel_map( def parallel_map(
@ -23,7 +26,10 @@ def parallel_map(
if not chunk_size: if not chunk_size:
chunk_size = max(1, ceil(len(values) / concurrency / 10)) chunk_size = max(1, ceil(len(values) / concurrency / 10))
logger.info(f"Starting parallel map, concurrency: {concurrency}, chunk size: {chunk_size}")
if concurrency == 1 or len(values) <= chunk_size: if concurrency == 1 or len(values) <= chunk_size:
logger.warn(f"Running in series, there is no reason for parallelism")
iterable = values if disable_progress else tqdm(values) iterable = values if disable_progress else tqdm(values)
return [function(v) for v in iterable] return [function(v) for v in iterable]