Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge branch 'master' into dev/config-toml
  • Loading branch information
ErikBjare committed Jun 15, 2021
2 parents f4b1bb8 + aa14c8b commit 043cc3c
Show file tree
Hide file tree
Showing 54 changed files with 1,619 additions and 1,027 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-18.04, windows-latest, macOS-latest]
python_version: [3.6]
python_version: [3.7, 3.9]
steps:
- uses: actions/checkout@v2
with:
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/lint.yml
@@ -0,0 +1,15 @@
name: Lint

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: psf/black@stable
29 changes: 21 additions & 8 deletions README.md
@@ -1,18 +1,31 @@
aw-core
=======

[![Build Status Travis](https://travis-ci.org/ActivityWatch/aw-core.svg?branch=master)](https://travis-ci.org/ActivityWatch/aw-core)
[![Build Status Appveyor](https://ci.appveyor.com/api/projects/status/h5cvxoghh1wr4ycr/branch/master?svg=true)](https://ci.appveyor.com/project/ErikBjare/aw-core/branch/master)
[![codecov](https://codecov.io/gh/ActivityWatch/aw-core/branch/master/graph/badge.svg)](https://codecov.io/gh/ActivityWatch/aw-core)
[![GitHub Actions badge](https://github.com/ActivityWatch/aw-core/workflows/Build/badge.svg)](https://github.com/ActivityWatch/aw-core/actions)
[![Code coverage](https://codecov.io/gh/ActivityWatch/aw-core/branch/master/graph/badge.svg)](https://codecov.io/gh/ActivityWatch/aw-core)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Typechecking: Mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)


Core library for ActivityWatch.


Contents
========
## Modules

- Models
- Schemas
- Filtering algorithms for sensitive data
- `aw_core`, contains basic datatypes and utilities, such as the `Event` class, helpers for configuration and logging, as well as schemas for buckets, events, and exports.
- `aw_datastore`, contains the datastore classes used by aw-server-python.
- `aw_transform`, all event-transforms used in queries.
- `aw_query`, the query-language used by ActivityWatch.


## How to install

To install the latest git version directly from github without cloning, run
`pip install git+https://github.com/ActivityWatch/aw-core.git`

To install from a cloned version, cd into the directory and run
`poetry install` to install inside an virtualenv. If you want to install it
system-wide it can be installed with `pip install .`, but that has the issue
that it might not get the exact version of the dependencies due to not reading
the poetry.lock file.

3 changes: 0 additions & 3 deletions aw_core/__init__.py
Expand Up @@ -4,9 +4,6 @@

from . import __about__

# TODO timeperiod should be moved to a seperate library, has uses outside of ActivityWatch
from .timeperiod import TimePeriod

from . import decorators
from . import util

Expand Down
3 changes: 3 additions & 0 deletions aw_core/config.py
Expand Up @@ -109,3 +109,6 @@ def save_config(appname, config):
config_file_path = os.path.join(config_dir, "{}.ini".format(appname))
with open(config_file_path, "w") as f:
config.write(f)
# Flush and fsync to lower risk of corrupted files
f.flush()
os.fsync(f.fileno())
21 changes: 16 additions & 5 deletions aw_core/decorators.py
Expand Up @@ -20,9 +20,15 @@ def g(*args, **kwargs):
# TODO: Use logging module instead?
nonlocal warned_for
if not warned_for:
warnings.simplefilter('always', DeprecationWarning) # turn off filter
warnings.warn("Call to deprecated function {}, this warning will only show once per function.".format(f.__name__), category=DeprecationWarning, stacklevel=2)
warnings.simplefilter('default', DeprecationWarning) # reset filter
warnings.simplefilter("always", DeprecationWarning) # turn off filter
warnings.warn(
"Call to deprecated function {}, this warning will only show once per function.".format(
f.__name__
),
category=DeprecationWarning,
stacklevel=2,
)
warnings.simplefilter("default", DeprecationWarning) # reset filter
warned_for = True
return f(*args, **kwargs)

Expand All @@ -37,7 +43,12 @@ def g(*args, **kwargs):
f(*args, **kwargs)
except exception as e:
# TODO: Use warnings module instead?
logging.error("{} crashed due to exception, restarting.".format(f.__name__))
logging.error(
"{} crashed due to exception, restarting.".format(f.__name__)
)
logging.error(e)
time.sleep(delay) # To prevent extremely fast restarts in case of bad state.
time.sleep(
delay
) # To prevent extremely fast restarts in case of bad state.

return g
1 change: 1 addition & 0 deletions aw_core/dirs.py
Expand Up @@ -18,6 +18,7 @@ def wrapper(subpath: Optional[str]) -> str:
path = f(subpath)
ensure_path_exists(path)
return path

return wrapper


Expand Down
68 changes: 48 additions & 20 deletions aw_core/log.py
Expand Up @@ -20,29 +20,52 @@ def get_log_file_path() -> Optional[str]: # pragma: no cover
return log_file_path


def setup_logging(name: str, testing=False, verbose=False,
log_stderr=True, log_file=False, log_file_json=False): # pragma: no cover
def setup_logging(
name: str,
testing=False,
verbose=False,
log_stderr=True,
log_file=False,
log_file_json=False,
): # pragma: no cover
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG if verbose else logging.INFO)
root_logger.handlers = []

if log_stderr:
root_logger.addHandler(_create_stderr_handler())
if log_file:
root_logger.addHandler(_create_file_handler(name, testing=testing, log_json=log_file_json))
root_logger.addHandler(
_create_file_handler(name, testing=testing, log_json=log_file_json)
)

def excepthook(type_, value, traceback):
root_logger.exception("Unhandled exception", exc_info=(type_, value, traceback))
# call the default excepthook if log_stderr isn't true (otherwise it'll just get duplicated)
if not log_stderr:
sys.__excepthook__(type_, value, traceback)

sys.excepthook = excepthook


def _get_latest_log_files(name, testing=False) -> List[str]: # pragma: no cover
"""Returns a list with the paths of all available logfiles for `name` sorted by latest first."""
log_dir = dirs.get_log_dir(name)
files = filter(lambda filename: name in filename, os.listdir(log_dir))
files = filter(lambda filename: "testing" in filename if testing else "testing" not in filename, files)
files = filter(
lambda filename: "testing" in filename
if testing
else "testing" not in filename,
files,
)
return [os.path.join(log_dir, filename) for filename in sorted(files, reverse=True)]


def get_latest_log_file(name, testing=False) -> Optional[str]: # pragma: no cover
"""Returns the filename of the last logfile with `name`.
Useful when you want to read the logfile of another ActivityWatch service."""
"""
Returns the filename of the last logfile with ``name``.
Useful when you want to read the logfile of another ActivityWatch service.
"""
last_logs = _get_latest_log_files(name, testing=testing)
return last_logs[0] if last_logs else None

Expand All @@ -54,7 +77,9 @@ def _create_stderr_handler() -> logging.Handler: # pragma: no cover
return stderr_handler


def _create_file_handler(name, testing=False, log_json=False) -> logging.Handler: # pragma: no cover
def _create_file_handler(
name, testing=False, log_json=False
) -> logging.Handler: # pragma: no cover
log_dir = dirs.get_log_dir(name)

# Set logfile path and name
Expand All @@ -67,7 +92,7 @@ def _create_file_handler(name, testing=False, log_json=False) -> logging.Handler
log_name = name + "_" + ("testing_" if testing else "") + now_str + file_ext
log_file_path = os.path.join(log_dir, log_name)

fh = logging.FileHandler(log_file_path, mode='w')
fh = logging.FileHandler(log_file_path, mode="w")
if log_json:
fh.setFormatter(_create_json_formatter())
else:
Expand All @@ -77,23 +102,26 @@ def _create_file_handler(name, testing=False, log_json=False) -> logging.Handler


def _create_human_formatter() -> logging.Formatter: # pragma: no cover
return logging.Formatter('%(asctime)s [%(levelname)-5s]: %(message)s (%(name)s:%(lineno)s)', '%Y-%m-%d %H:%M:%S')
return logging.Formatter(
"%(asctime)s [%(levelname)-5s]: %(message)s (%(name)s:%(lineno)s)",
"%Y-%m-%d %H:%M:%S",
)


def _create_json_formatter() -> logging.Formatter: # pragma: no cover
supported_keys = [
'asctime',
"asctime",
# 'created',
'filename',
'funcName',
'levelname',
"filename",
"funcName",
"levelname",
# 'levelno',
'lineno',
'module',
"lineno",
"module",
# 'msecs',
'message',
'name',
'pathname',
"message",
"name",
"pathname",
# 'process',
# 'processName',
# 'relativeCreated',
Expand All @@ -103,8 +131,8 @@ def _create_json_formatter() -> logging.Formatter: # pragma: no cover

def log_format(x):
"""Used to give JsonFormatter proper parameter format"""
return ['%({0:s})'.format(i) for i in x]
return ["%({0:s})".format(i) for i in x]

custom_format = ' '.join(log_format(supported_keys))
custom_format = " ".join(log_format(supported_keys))

return jsonlogger.JsonFormatter(custom_format)
35 changes: 27 additions & 8 deletions aw_core/models.py
Expand Up @@ -40,11 +40,18 @@ class Event(dict):
Used to represents an event.
"""

def __init__(self, id: Id = None, timestamp: ConvertableTimestamp = None,
duration: Duration = 0, data: Data = dict()) -> None:
def __init__(
self,
id: Id = None,
timestamp: ConvertableTimestamp = None,
duration: Duration = 0,
data: Data = dict(),
) -> None:
self.id = id
if timestamp is None:
logger.warning("Event initializer did not receive a timestamp argument, using now as timestamp")
logger.warning(
"Event initializer did not receive a timestamp argument, using now as timestamp"
)
# FIXME: The typing.cast here was required for mypy to shut up, weird...
self.timestamp = datetime.now(typing.cast(timezone, timezone.utc))
else:
Expand All @@ -55,17 +62,27 @@ def __init__(self, id: Id = None, timestamp: ConvertableTimestamp = None,

def __eq__(self, other: object) -> bool:
if isinstance(other, Event):
return self.timestamp == other.timestamp \
and self.duration == other.duration \
return (
self.timestamp == other.timestamp
and self.duration == other.duration
and self.data == other.data
)
else:
raise TypeError("operator not supported between instances of '{}' and '{}'".format(type(self), type(other)))
raise TypeError(
"operator not supported between instances of '{}' and '{}'".format(
type(self), type(other)
)
)

def __lt__(self, other: object) -> bool:
if isinstance(other, Event):
return self.timestamp < other.timestamp
else:
raise TypeError("operator not supported between instances of '{}' and '{}'".format(type(self), type(other)))
raise TypeError(
"operator not supported between instances of '{}' and '{}'".format(
type(self), type(other)
)
)

def to_json_dict(self) -> dict:
"""Useful when sending data over the wire.
Expand Down Expand Up @@ -119,4 +136,6 @@ def duration(self, duration: Duration) -> None:
elif isinstance(duration, numbers.Real):
self["duration"] = timedelta(seconds=duration) # type: ignore
else:
raise TypeError("Couldn't parse duration of invalid type {}".format(type(duration)))
raise TypeError(
"Couldn't parse duration of invalid type {}".format(type(duration))
)
Empty file added aw_core/py.typed
Empty file.

0 comments on commit 043cc3c

Please sign in to comment.