Skip to content

Commit 043cc3c

Browse files
committed
Merge branch 'master' into dev/config-toml
2 parents f4b1bb8 + aa14c8b commit 043cc3c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1619
-1027
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
strategy:
1616
matrix:
1717
os: [ubuntu-18.04, windows-latest, macOS-latest]
18-
python_version: [3.6]
18+
python_version: [3.7, 3.9]
1919
steps:
2020
- uses: actions/checkout@v2
2121
with:

.github/workflows/lint.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: Lint
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
pull_request:
7+
branches: [ master ]
8+
9+
jobs:
10+
lint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v2
14+
- uses: actions/setup-python@v2
15+
- uses: psf/black@stable

README.md

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,31 @@
11
aw-core
22
=======
33

4-
[![Build Status Travis](https://travis-ci.org/ActivityWatch/aw-core.svg?branch=master)](https://travis-ci.org/ActivityWatch/aw-core)
5-
[![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)
6-
[![codecov](https://codecov.io/gh/ActivityWatch/aw-core/branch/master/graph/badge.svg)](https://codecov.io/gh/ActivityWatch/aw-core)
4+
[![GitHub Actions badge](https://github.com/ActivityWatch/aw-core/workflows/Build/badge.svg)](https://github.com/ActivityWatch/aw-core/actions)
5+
[![Code coverage](https://codecov.io/gh/ActivityWatch/aw-core/branch/master/graph/badge.svg)](https://codecov.io/gh/ActivityWatch/aw-core)
6+
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
7+
[![Typechecking: Mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
78

89

910
Core library for ActivityWatch.
1011

1112

12-
Contents
13-
========
13+
## Modules
1414

15-
- Models
16-
- Schemas
17-
- Filtering algorithms for sensitive data
15+
- `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.
16+
- `aw_datastore`, contains the datastore classes used by aw-server-python.
17+
- `aw_transform`, all event-transforms used in queries.
18+
- `aw_query`, the query-language used by ActivityWatch.
19+
20+
21+
## How to install
22+
23+
To install the latest git version directly from github without cloning, run
24+
`pip install git+https://github.com/ActivityWatch/aw-core.git`
25+
26+
To install from a cloned version, cd into the directory and run
27+
`poetry install` to install inside an virtualenv. If you want to install it
28+
system-wide it can be installed with `pip install .`, but that has the issue
29+
that it might not get the exact version of the dependencies due to not reading
30+
the poetry.lock file.
1831

aw_core/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44

55
from . import __about__
66

7-
# TODO timeperiod should be moved to a seperate library, has uses outside of ActivityWatch
8-
from .timeperiod import TimePeriod
9-
107
from . import decorators
118
from . import util
129

aw_core/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,6 @@ def save_config(appname, config):
109109
config_file_path = os.path.join(config_dir, "{}.ini".format(appname))
110110
with open(config_file_path, "w") as f:
111111
config.write(f)
112+
# Flush and fsync to lower risk of corrupted files
113+
f.flush()
114+
os.fsync(f.fileno())

aw_core/decorators.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,15 @@ def g(*args, **kwargs):
2020
# TODO: Use logging module instead?
2121
nonlocal warned_for
2222
if not warned_for:
23-
warnings.simplefilter('always', DeprecationWarning) # turn off filter
24-
warnings.warn("Call to deprecated function {}, this warning will only show once per function.".format(f.__name__), category=DeprecationWarning, stacklevel=2)
25-
warnings.simplefilter('default', DeprecationWarning) # reset filter
23+
warnings.simplefilter("always", DeprecationWarning) # turn off filter
24+
warnings.warn(
25+
"Call to deprecated function {}, this warning will only show once per function.".format(
26+
f.__name__
27+
),
28+
category=DeprecationWarning,
29+
stacklevel=2,
30+
)
31+
warnings.simplefilter("default", DeprecationWarning) # reset filter
2632
warned_for = True
2733
return f(*args, **kwargs)
2834

@@ -37,7 +43,12 @@ def g(*args, **kwargs):
3743
f(*args, **kwargs)
3844
except exception as e:
3945
# TODO: Use warnings module instead?
40-
logging.error("{} crashed due to exception, restarting.".format(f.__name__))
46+
logging.error(
47+
"{} crashed due to exception, restarting.".format(f.__name__)
48+
)
4149
logging.error(e)
42-
time.sleep(delay) # To prevent extremely fast restarts in case of bad state.
50+
time.sleep(
51+
delay
52+
) # To prevent extremely fast restarts in case of bad state.
53+
4354
return g

aw_core/dirs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def wrapper(subpath: Optional[str]) -> str:
1818
path = f(subpath)
1919
ensure_path_exists(path)
2020
return path
21+
2122
return wrapper
2223

2324

aw_core/log.py

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,52 @@ def get_log_file_path() -> Optional[str]: # pragma: no cover
2020
return log_file_path
2121

2222

23-
def setup_logging(name: str, testing=False, verbose=False,
24-
log_stderr=True, log_file=False, log_file_json=False): # pragma: no cover
23+
def setup_logging(
24+
name: str,
25+
testing=False,
26+
verbose=False,
27+
log_stderr=True,
28+
log_file=False,
29+
log_file_json=False,
30+
): # pragma: no cover
2531
root_logger = logging.getLogger()
2632
root_logger.setLevel(logging.DEBUG if verbose else logging.INFO)
2733
root_logger.handlers = []
2834

2935
if log_stderr:
3036
root_logger.addHandler(_create_stderr_handler())
3137
if log_file:
32-
root_logger.addHandler(_create_file_handler(name, testing=testing, log_json=log_file_json))
38+
root_logger.addHandler(
39+
_create_file_handler(name, testing=testing, log_json=log_file_json)
40+
)
41+
42+
def excepthook(type_, value, traceback):
43+
root_logger.exception("Unhandled exception", exc_info=(type_, value, traceback))
44+
# call the default excepthook if log_stderr isn't true (otherwise it'll just get duplicated)
45+
if not log_stderr:
46+
sys.__excepthook__(type_, value, traceback)
47+
48+
sys.excepthook = excepthook
3349

3450

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

4263

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

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

5679

57-
def _create_file_handler(name, testing=False, log_json=False) -> logging.Handler: # pragma: no cover
80+
def _create_file_handler(
81+
name, testing=False, log_json=False
82+
) -> logging.Handler: # pragma: no cover
5883
log_dir = dirs.get_log_dir(name)
5984

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

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

78103

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

82110

83111
def _create_json_formatter() -> logging.Formatter: # pragma: no cover
84112
supported_keys = [
85-
'asctime',
113+
"asctime",
86114
# 'created',
87-
'filename',
88-
'funcName',
89-
'levelname',
115+
"filename",
116+
"funcName",
117+
"levelname",
90118
# 'levelno',
91-
'lineno',
92-
'module',
119+
"lineno",
120+
"module",
93121
# 'msecs',
94-
'message',
95-
'name',
96-
'pathname',
122+
"message",
123+
"name",
124+
"pathname",
97125
# 'process',
98126
# 'processName',
99127
# 'relativeCreated',
@@ -103,8 +131,8 @@ def _create_json_formatter() -> logging.Formatter: # pragma: no cover
103131

104132
def log_format(x):
105133
"""Used to give JsonFormatter proper parameter format"""
106-
return ['%({0:s})'.format(i) for i in x]
134+
return ["%({0:s})".format(i) for i in x]
107135

108-
custom_format = ' '.join(log_format(supported_keys))
136+
custom_format = " ".join(log_format(supported_keys))
109137

110138
return jsonlogger.JsonFormatter(custom_format)

aw_core/models.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,18 @@ class Event(dict):
4040
Used to represents an event.
4141
"""
4242

43-
def __init__(self, id: Id = None, timestamp: ConvertableTimestamp = None,
44-
duration: Duration = 0, data: Data = dict()) -> None:
43+
def __init__(
44+
self,
45+
id: Id = None,
46+
timestamp: ConvertableTimestamp = None,
47+
duration: Duration = 0,
48+
data: Data = dict(),
49+
) -> None:
4550
self.id = id
4651
if timestamp is None:
47-
logger.warning("Event initializer did not receive a timestamp argument, using now as timestamp")
52+
logger.warning(
53+
"Event initializer did not receive a timestamp argument, using now as timestamp"
54+
)
4855
# FIXME: The typing.cast here was required for mypy to shut up, weird...
4956
self.timestamp = datetime.now(typing.cast(timezone, timezone.utc))
5057
else:
@@ -55,17 +62,27 @@ def __init__(self, id: Id = None, timestamp: ConvertableTimestamp = None,
5562

5663
def __eq__(self, other: object) -> bool:
5764
if isinstance(other, Event):
58-
return self.timestamp == other.timestamp \
59-
and self.duration == other.duration \
65+
return (
66+
self.timestamp == other.timestamp
67+
and self.duration == other.duration
6068
and self.data == other.data
69+
)
6170
else:
62-
raise TypeError("operator not supported between instances of '{}' and '{}'".format(type(self), type(other)))
71+
raise TypeError(
72+
"operator not supported between instances of '{}' and '{}'".format(
73+
type(self), type(other)
74+
)
75+
)
6376

6477
def __lt__(self, other: object) -> bool:
6578
if isinstance(other, Event):
6679
return self.timestamp < other.timestamp
6780
else:
68-
raise TypeError("operator not supported between instances of '{}' and '{}'".format(type(self), type(other)))
81+
raise TypeError(
82+
"operator not supported between instances of '{}' and '{}'".format(
83+
type(self), type(other)
84+
)
85+
)
6986

7087
def to_json_dict(self) -> dict:
7188
"""Useful when sending data over the wire.
@@ -119,4 +136,6 @@ def duration(self, duration: Duration) -> None:
119136
elif isinstance(duration, numbers.Real):
120137
self["duration"] = timedelta(seconds=duration) # type: ignore
121138
else:
122-
raise TypeError("Couldn't parse duration of invalid type {}".format(type(duration)))
139+
raise TypeError(
140+
"Couldn't parse duration of invalid type {}".format(type(duration))
141+
)

aw_core/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)