vk-scripts 1.0.5__tar.gz → 1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vk_scripts-1.0.5 → vk_scripts-1.1}/.github/workflows/ci.yml +4 -3
- vk_scripts-1.1/.pylintrc +8 -0
- {vk_scripts-1.0.5/vk_scripts.egg-info → vk_scripts-1.1}/PKG-INFO +2 -4
- {vk_scripts-1.0.5 → vk_scripts-1.1}/README.md +0 -2
- {vk_scripts-1.0.5 → vk_scripts-1.1}/docs/mutuals.md +6 -11
- {vk_scripts-1.0.5 → vk_scripts-1.1}/docs/sessions.md +0 -7
- {vk_scripts-1.0.5 → vk_scripts-1.1}/docs/status.md +0 -7
- {vk_scripts-1.0.5 → vk_scripts-1.1}/pyproject.toml +1 -4
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/api.py +18 -18
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/last_seen.py +9 -15
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/mutuals.py +23 -18
- vk_scripts-1.1/vk/platform.py +39 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/tracking/__init__.py +2 -2
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/tracking/db/__init__.py +1 -1
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/tracking/db/backend/__init__.py +3 -3
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/tracking/db/backend/csv.py +2 -2
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/tracking/db/backend/log.py +9 -22
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/tracking/db/format.py +11 -11
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/tracking/db/record.py +2 -2
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/tracking/db/timestamp.py +2 -2
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/tracking/sessions.py +62 -63
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/tracking/status.py +26 -26
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/user.py +38 -56
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/utils/bar_chart.py +15 -15
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/utils/io.py +6 -6
- vk_scripts-1.1/vk/version.py +16 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1/vk_scripts.egg-info}/PKG-INFO +2 -4
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk_scripts.egg-info/SOURCES.txt +2 -0
- vk_scripts-1.1/vk_scripts.egg-info/scm_file_list.json +50 -0
- vk_scripts-1.1/vk_scripts.egg-info/scm_version.json +8 -0
- vk_scripts-1.0.5/.pylintrc +0 -7
- vk_scripts-1.0.5/vk/platform.py +0 -53
- vk_scripts-1.0.5/vk/version.py +0 -16
- {vk_scripts-1.0.5 → vk_scripts-1.1}/.git-blame-ignore-revs +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/.gitattributes +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/.github/dependabot.yml +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/.gitignore +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/LICENSE.txt +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/docs/images/date.png +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/docs/images/hour.png +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/docs/images/user.png +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/docs/images/weekday.png +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/setup.cfg +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/test/bin/main.sh +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/test/bin/mutuals.sh +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/test/bin/sessions.sh +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/test/bin/status.sh +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/test/bin/status_once.sh +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/test/lib/test.sh +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/test/share/test_db.csv +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/__init__.py +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/error.py +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/tracking/db/backend/null.py +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/tracking/db/meta.py +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk/utils/__init__.py +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk_scripts.egg-info/dependency_links.txt +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk_scripts.egg-info/entry_points.txt +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk_scripts.egg-info/requires.txt +0 -0
- {vk_scripts-1.0.5 → vk_scripts-1.1}/vk_scripts.egg-info/top_level.txt +0 -0
|
@@ -18,9 +18,10 @@ jobs:
|
|
|
18
18
|
lint:
|
|
19
19
|
runs-on: ubuntu-latest
|
|
20
20
|
name: Linting
|
|
21
|
+
continue-on-error: "${{ github.ref != 'refs/heads/master' }}"
|
|
21
22
|
steps:
|
|
22
23
|
- name: Checkout
|
|
23
|
-
uses: actions/checkout@
|
|
24
|
+
uses: actions/checkout@v7
|
|
24
25
|
with:
|
|
25
26
|
fetch-depth: 0
|
|
26
27
|
- name: Set up Python
|
|
@@ -40,7 +41,7 @@ jobs:
|
|
|
40
41
|
name: 'Python ${{ matrix.python-version }}'
|
|
41
42
|
steps:
|
|
42
43
|
- name: Checkout
|
|
43
|
-
uses: actions/checkout@
|
|
44
|
+
uses: actions/checkout@v7
|
|
44
45
|
with:
|
|
45
46
|
fetch-depth: 0
|
|
46
47
|
- name: Set up Python
|
|
@@ -63,7 +64,7 @@ jobs:
|
|
|
63
64
|
name: Publish
|
|
64
65
|
steps:
|
|
65
66
|
- name: Checkout
|
|
66
|
-
uses: actions/checkout@
|
|
67
|
+
uses: actions/checkout@v7
|
|
67
68
|
with:
|
|
68
69
|
fetch-depth: 0
|
|
69
70
|
- name: Set up Python
|
vk_scripts-1.1/.pylintrc
ADDED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vk_scripts
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1
|
|
4
4
|
Summary: Scripts to stalk people on VK
|
|
5
5
|
Author-email: Egor Tensin <egor@tensin.name>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/egor-tensin/vk-scripts
|
|
8
8
|
Project-URL: Bug Tracker, https://github.com/egor-tensin/vk-scripts/issues
|
|
9
9
|
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.6
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE.txt
|
|
13
13
|
Requires-Dist: importlib-metadata~=4.0; python_version < "3.8"
|
|
@@ -26,8 +26,6 @@ Installation
|
|
|
26
26
|
|
|
27
27
|
pip install vk-scripts
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
Python 3.4 or higher is required.
|
|
31
29
|
`vk-sessions` uses [matplotlib].
|
|
32
30
|
|
|
33
31
|
[matplotlib]: http://matplotlib.org/
|
|
@@ -13,8 +13,8 @@ Usage
|
|
|
13
13
|
For example (using made up user IDs/"screen names"),
|
|
14
14
|
|
|
15
15
|
> vk-mutuals john.doe jane.doe
|
|
16
|
-
89497105,John,Smith
|
|
17
|
-
3698577,Jane,Smith
|
|
16
|
+
89497105,John,Smith,john.smith
|
|
17
|
+
3698577,Jane,Smith,jane.smith
|
|
18
18
|
|
|
19
19
|
In the example above, both "John Doe" and "Jane Doe" are friends with "John
|
|
20
20
|
Smith" and "Jane Smith", whose user IDs are 89497105 and 3698577 respectively.
|
|
@@ -27,18 +27,13 @@ You can also get a JSON document:
|
|
|
27
27
|
{
|
|
28
28
|
"uid": 89497105,
|
|
29
29
|
"first_name": "John",
|
|
30
|
-
"last_name": "Smith"
|
|
30
|
+
"last_name": "Smith",
|
|
31
|
+
"domain": "john.smith"
|
|
31
32
|
},
|
|
32
33
|
{
|
|
33
34
|
"uid": 3698577,
|
|
34
35
|
"first_name": "Jane",
|
|
35
|
-
"last_name": "Smith"
|
|
36
|
+
"last_name": "Smith",
|
|
37
|
+
"domain": "jane.smith"
|
|
36
38
|
}
|
|
37
39
|
]
|
|
38
|
-
|
|
39
|
-
See also
|
|
40
|
-
--------
|
|
41
|
-
|
|
42
|
-
* [License]
|
|
43
|
-
|
|
44
|
-
[License]: ../README.md#license
|
|
@@ -159,10 +159,3 @@ Known issues
|
|
|
159
159
|
time (for example, just listening to music), they appear offline.
|
|
160
160
|
Hence the 0:00:00 durations you might sometimes encounter.
|
|
161
161
|
This might also happen using other clients.
|
|
162
|
-
|
|
163
|
-
See also
|
|
164
|
-
--------
|
|
165
|
-
|
|
166
|
-
* [License]
|
|
167
|
-
|
|
168
|
-
[License]: ../README.md#license
|
|
@@ -31,10 +31,3 @@ If you want to record when people go online/offline for further analysis using
|
|
|
31
31
|
[vk-sessions], specify the path to a database using the `--output` parameter.
|
|
32
32
|
|
|
33
33
|
[vk-sessions]: sessions.md
|
|
34
|
-
|
|
35
|
-
See also
|
|
36
|
-
--------
|
|
37
|
-
|
|
38
|
-
* [License]
|
|
39
|
-
|
|
40
|
-
[License]: ../README.md#license
|
|
@@ -12,7 +12,7 @@ license-files = ["LICENSE.txt"]
|
|
|
12
12
|
dynamic = ["version"]
|
|
13
13
|
authors = [{name = "Egor Tensin", email = "egor@tensin.name"}]
|
|
14
14
|
readme = "README.md"
|
|
15
|
-
requires-python = ">=3.
|
|
15
|
+
requires-python = ">=3.6"
|
|
16
16
|
|
|
17
17
|
dependencies = [
|
|
18
18
|
'importlib-metadata ~= 4.0 ; python_version < "3.8"',
|
|
@@ -33,6 +33,3 @@ vk-status = "vk.tracking.status:main"
|
|
|
33
33
|
vk-mutuals = "vk.mutuals:main"
|
|
34
34
|
|
|
35
35
|
[tool.setuptools_scm]
|
|
36
|
-
|
|
37
|
-
[tool.black]
|
|
38
|
-
skip-string-normalization = true
|
|
@@ -49,29 +49,29 @@ def _build_url(scheme, host, path, params=None, empty_params=False):
|
|
|
49
49
|
else:
|
|
50
50
|
raise TypeError()
|
|
51
51
|
path = urllib.parse.quote(path)
|
|
52
|
-
return urllib.parse.urlunsplit((scheme, host, path, params,
|
|
52
|
+
return urllib.parse.urlunsplit((scheme, host, path, params, ""))
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
def _join_param_values(values):
|
|
56
56
|
if isinstance(values, str):
|
|
57
57
|
return values
|
|
58
58
|
if isinstance(values, Iterable):
|
|
59
|
-
return
|
|
59
|
+
return ",".join(map(str, values))
|
|
60
60
|
return values
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
def _join_path(base, url):
|
|
64
|
-
if not base.endswith(
|
|
65
|
-
base +=
|
|
64
|
+
if not base.endswith("/"):
|
|
65
|
+
base += "/"
|
|
66
66
|
return urllib.parse.urljoin(base, url)
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
ACCESS_TOKEN =
|
|
69
|
+
ACCESS_TOKEN = "9722cef09722cef09722cef071974b8cbe997229722cef0cbabfd816916af6c7bd37006"
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
class Version(Enum):
|
|
73
73
|
# https://dev.vk.com/en/reference/versions
|
|
74
|
-
V5_199 =
|
|
74
|
+
V5_199 = "5.199"
|
|
75
75
|
DEFAULT = V5_199
|
|
76
76
|
|
|
77
77
|
def __str__(self):
|
|
@@ -79,7 +79,7 @@ class Version(Enum):
|
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
class Language(Enum):
|
|
82
|
-
EN =
|
|
82
|
+
EN = "en"
|
|
83
83
|
DEFAULT = EN
|
|
84
84
|
|
|
85
85
|
def __str__(self):
|
|
@@ -87,24 +87,24 @@ class Language(Enum):
|
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
class Method(Enum):
|
|
90
|
-
USERS_GET =
|
|
91
|
-
FRIENDS_GET =
|
|
90
|
+
USERS_GET = "users.get"
|
|
91
|
+
FRIENDS_GET = "friends.get"
|
|
92
92
|
|
|
93
93
|
def __str__(self):
|
|
94
94
|
return self.value
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
class CommonParameters(Enum):
|
|
98
|
-
ACCESS_TOKEN =
|
|
99
|
-
VERSION =
|
|
100
|
-
LANGUAGE =
|
|
98
|
+
ACCESS_TOKEN = "access_token"
|
|
99
|
+
VERSION = "v"
|
|
100
|
+
LANGUAGE = "lang"
|
|
101
101
|
|
|
102
102
|
def __str__(self):
|
|
103
103
|
return self.value
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
class API:
|
|
107
|
-
_ROOT_URL =
|
|
107
|
+
_ROOT_URL = "https://api.vk.com/method/"
|
|
108
108
|
|
|
109
109
|
_SCHEME, _HOST, _ROOT_PATH = _split_url(_ROOT_URL)
|
|
110
110
|
|
|
@@ -127,9 +127,9 @@ class API:
|
|
|
127
127
|
with urlopen(url) as response:
|
|
128
128
|
response = json.loads(response.read().decode())
|
|
129
129
|
# print(response)
|
|
130
|
-
if
|
|
130
|
+
if "response" not in response:
|
|
131
131
|
raise vk.error.InvalidAPIResponseError(response)
|
|
132
|
-
return response[
|
|
132
|
+
return response["response"]
|
|
133
133
|
except (ConnectionError, URLError) as e:
|
|
134
134
|
raise vk.error.APIConnectionError(str(e)) from e
|
|
135
135
|
|
|
@@ -138,7 +138,7 @@ class API:
|
|
|
138
138
|
user_list = map(User.from_api_response, user_list)
|
|
139
139
|
if deactivated_users:
|
|
140
140
|
return user_list
|
|
141
|
-
return [user for user in user_list if not user.is_deactivated
|
|
141
|
+
return [user for user in user_list if not user.is_deactivated]
|
|
142
142
|
|
|
143
143
|
def users_get(self, user_ids, fields=(), deactivated_users=True):
|
|
144
144
|
return self._filter_response_with_users(
|
|
@@ -154,6 +154,6 @@ class API:
|
|
|
154
154
|
response = self._call_method(
|
|
155
155
|
Method.FRIENDS_GET, user_id=user_id, fields=_join_param_values(fields)
|
|
156
156
|
)
|
|
157
|
-
if
|
|
157
|
+
if "items" not in response:
|
|
158
158
|
raise vk.error.InvalidAPIResponseError(response)
|
|
159
|
-
return self._filter_response_with_users(response[
|
|
159
|
+
return self._filter_response_with_users(response["items"], deactivated_users)
|
|
@@ -31,8 +31,8 @@ def _parse_platform(x):
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
class LastSeenField(Enum):
|
|
34
|
-
TIME =
|
|
35
|
-
PLATFORM =
|
|
34
|
+
TIME = "time"
|
|
35
|
+
PLATFORM = "platform"
|
|
36
36
|
|
|
37
37
|
def __str__(self):
|
|
38
38
|
return self.value
|
|
@@ -80,20 +80,14 @@ class LastSeen(MutableMapping):
|
|
|
80
80
|
|
|
81
81
|
_DEFAULT_FIELD_PARSER = str
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def get_time(self):
|
|
83
|
+
@property
|
|
84
|
+
def time(self):
|
|
87
85
|
return self[LastSeenField.TIME]
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def has_platform(self):
|
|
93
|
-
return LastSeenField.PLATFORM in self
|
|
87
|
+
@property
|
|
88
|
+
def time_local(self):
|
|
89
|
+
return self.time.astimezone()
|
|
94
90
|
|
|
95
|
-
|
|
91
|
+
@property
|
|
92
|
+
def platform(self):
|
|
96
93
|
return self[LastSeenField.PLATFORM]
|
|
97
|
-
|
|
98
|
-
def set_platform(self, platform):
|
|
99
|
-
self[LastSeenField.PLATFORM] = platform
|
|
@@ -14,11 +14,16 @@ from vk.user import UserField
|
|
|
14
14
|
from vk.utils import io
|
|
15
15
|
import vk.version
|
|
16
16
|
|
|
17
|
-
_OUTPUT_USER_FIELDS =
|
|
17
|
+
_OUTPUT_USER_FIELDS = (
|
|
18
|
+
UserField.UID,
|
|
19
|
+
UserField.FIRST_NAME,
|
|
20
|
+
UserField.LAST_NAME,
|
|
21
|
+
UserField.DOMAIN,
|
|
22
|
+
)
|
|
18
23
|
|
|
19
24
|
|
|
20
25
|
def _query_friend_list(api, user):
|
|
21
|
-
return api.friends_get(user.
|
|
26
|
+
return api.friends_get(user.uid, fields=_OUTPUT_USER_FIELDS)
|
|
22
27
|
|
|
23
28
|
|
|
24
29
|
def _filter_user_fields(user):
|
|
@@ -52,8 +57,8 @@ class OutputSinkJSON(OutputSinkMutualFriends):
|
|
|
52
57
|
|
|
53
58
|
|
|
54
59
|
class OutputFormat(Enum):
|
|
55
|
-
CSV =
|
|
56
|
-
JSON =
|
|
60
|
+
CSV = "csv"
|
|
61
|
+
JSON = "json"
|
|
57
62
|
|
|
58
63
|
def __str__(self):
|
|
59
64
|
return self.value
|
|
@@ -67,14 +72,14 @@ class OutputFormat(Enum):
|
|
|
67
72
|
return OutputSinkCSV(fd)
|
|
68
73
|
if self is OutputFormat.JSON:
|
|
69
74
|
return OutputSinkJSON(fd)
|
|
70
|
-
raise NotImplementedError(
|
|
75
|
+
raise NotImplementedError(f"unsupported output format: {self}")
|
|
71
76
|
|
|
72
77
|
|
|
73
78
|
def _parse_output_format(s):
|
|
74
79
|
try:
|
|
75
80
|
return OutputFormat(s)
|
|
76
81
|
except ValueError:
|
|
77
|
-
raise argparse.ArgumentTypeError(
|
|
82
|
+
raise argparse.ArgumentTypeError(f"invalid output format: {s}")
|
|
78
83
|
|
|
79
84
|
|
|
80
85
|
def _parse_args(args=None):
|
|
@@ -82,29 +87,29 @@ def _parse_args(args=None):
|
|
|
82
87
|
args = sys.argv[1:]
|
|
83
88
|
|
|
84
89
|
parser = argparse.ArgumentParser(
|
|
85
|
-
description=
|
|
90
|
+
description="Learn who your ex and her new boyfriend are both friends with."
|
|
86
91
|
)
|
|
87
92
|
|
|
88
93
|
vk.version.add_to_arg_parser(parser)
|
|
89
94
|
|
|
90
95
|
parser.add_argument(
|
|
91
|
-
|
|
96
|
+
"uids", metavar="UID", nargs="+", help='user IDs or "screen names"'
|
|
92
97
|
)
|
|
93
98
|
parser.add_argument(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
dest=
|
|
99
|
+
"-f",
|
|
100
|
+
"--format",
|
|
101
|
+
dest="out_fmt",
|
|
97
102
|
type=_parse_output_format,
|
|
98
103
|
default=OutputFormat.CSV,
|
|
99
104
|
choices=OutputFormat,
|
|
100
|
-
help=
|
|
105
|
+
help="specify output format",
|
|
101
106
|
)
|
|
102
107
|
parser.add_argument(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
metavar=
|
|
106
|
-
dest=
|
|
107
|
-
help=
|
|
108
|
+
"-o",
|
|
109
|
+
"--output",
|
|
110
|
+
metavar="PATH",
|
|
111
|
+
dest="out_path",
|
|
112
|
+
help="set output file path (standard output by default)",
|
|
108
113
|
)
|
|
109
114
|
|
|
110
115
|
return parser.parse_args(args)
|
|
@@ -127,5 +132,5 @@ def main(args=None):
|
|
|
127
132
|
write_mutual_friends(**vars(_parse_args(args)))
|
|
128
133
|
|
|
129
134
|
|
|
130
|
-
if __name__ ==
|
|
135
|
+
if __name__ == "__main__":
|
|
131
136
|
main()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Copyright (c) 2016 Egor Tensin <egor@tensin.name>
|
|
2
|
+
# This file is part of the "VK scripts" project.
|
|
3
|
+
# For details, see https://github.com/egor-tensin/vk-scripts.
|
|
4
|
+
# Distributed under the MIT License.
|
|
5
|
+
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Platform(Enum):
|
|
10
|
+
# https://dev.vk.com/en/reference/objects/user#last_seen
|
|
11
|
+
MOBILE = 1
|
|
12
|
+
IPHONE = 2
|
|
13
|
+
IPAD = 3
|
|
14
|
+
ANDROID = 4
|
|
15
|
+
WINDOWS_PHONE = 5
|
|
16
|
+
WINDOWS10 = 6
|
|
17
|
+
WEB = 7
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def from_string(s):
|
|
21
|
+
return Platform(int(s))
|
|
22
|
+
|
|
23
|
+
def __str__(self):
|
|
24
|
+
return str(self.value)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def descr(self):
|
|
28
|
+
return f"the {_PLATFORM_DESCRIPTIONS[self]}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_PLATFORM_DESCRIPTIONS = {
|
|
32
|
+
Platform.MOBILE: '"mobile" web version (or an unrecognized mobile app)',
|
|
33
|
+
Platform.IPHONE: "official iPhone app",
|
|
34
|
+
Platform.IPAD: "official iPad app",
|
|
35
|
+
Platform.ANDROID: "official Android app",
|
|
36
|
+
Platform.WINDOWS_PHONE: "official Windows Phone app",
|
|
37
|
+
Platform.WINDOWS10: "official Windows 10 app",
|
|
38
|
+
Platform.WEB: "web version (or an unrecognized app)",
|
|
39
|
+
}
|
|
@@ -43,6 +43,6 @@ class Reader(meta.Reader):
|
|
|
43
43
|
@staticmethod
|
|
44
44
|
def _record_from_row(row):
|
|
45
45
|
record = Record(Timestamp.from_string(row[0]))
|
|
46
|
-
for i in
|
|
47
|
-
record[
|
|
46
|
+
for i, field in enumerate(Record.FIELDS):
|
|
47
|
+
record[field] = row[i + 1]
|
|
48
48
|
return record
|
|
@@ -15,7 +15,7 @@ class Writer(meta.Writer):
|
|
|
15
15
|
handler = logging.StreamHandler(fd)
|
|
16
16
|
handler.setFormatter(
|
|
17
17
|
logging.Formatter(
|
|
18
|
-
fmt=
|
|
18
|
+
fmt="[%(asctime)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S%z"
|
|
19
19
|
)
|
|
20
20
|
)
|
|
21
21
|
self._logger.addHandler(handler)
|
|
@@ -47,7 +47,7 @@ class Writer(meta.Writer):
|
|
|
47
47
|
|
|
48
48
|
def on_initial_status(self, user):
|
|
49
49
|
self._reset_last_notification()
|
|
50
|
-
if user.is_online
|
|
50
|
+
if user.is_online:
|
|
51
51
|
self.info(self._format_user_is_online(user))
|
|
52
52
|
else:
|
|
53
53
|
self.info(self._format_user_is_offline(user))
|
|
@@ -55,7 +55,7 @@ class Writer(meta.Writer):
|
|
|
55
55
|
|
|
56
56
|
def on_status_update(self, user):
|
|
57
57
|
self._reset_last_notification()
|
|
58
|
-
if user.is_online
|
|
58
|
+
if user.is_online:
|
|
59
59
|
self.info(self._format_user_went_online(user))
|
|
60
60
|
else:
|
|
61
61
|
self.info(self._format_user_went_offline(user))
|
|
@@ -69,39 +69,26 @@ class Writer(meta.Writer):
|
|
|
69
69
|
self._set_last_notification(e)
|
|
70
70
|
self.exception(e)
|
|
71
71
|
|
|
72
|
-
@staticmethod
|
|
73
|
-
def _format_user(user):
|
|
74
|
-
if user.has_last_name():
|
|
75
|
-
return '{} {}'.format(user.get_first_name(), user.get_last_name())
|
|
76
|
-
return '{}'.format(user.get_first_name())
|
|
77
|
-
|
|
78
72
|
@staticmethod
|
|
79
73
|
def _format_user_is_online(user):
|
|
80
|
-
return
|
|
74
|
+
return f"{user.full_name} is ONLINE."
|
|
81
75
|
|
|
82
76
|
@staticmethod
|
|
83
77
|
def _format_user_is_offline(user):
|
|
84
|
-
return
|
|
78
|
+
return f"{user.full_name} is OFFLINE."
|
|
85
79
|
|
|
86
80
|
@staticmethod
|
|
87
81
|
def _format_user_last_seen(user):
|
|
88
|
-
return
|
|
89
|
-
Writer._format_user(user),
|
|
90
|
-
user.get_last_seen_time_local(),
|
|
91
|
-
user.get_last_seen_platform().get_descr_text(),
|
|
92
|
-
)
|
|
82
|
+
return f"{user.full_name} was last seen at {user.last_seen.time_local} using {user.last_seen.platform.descr}."
|
|
93
83
|
|
|
94
84
|
@staticmethod
|
|
95
85
|
def _format_user_went_online(user):
|
|
96
|
-
return
|
|
86
|
+
return f"{user.full_name} went ONLINE."
|
|
97
87
|
|
|
98
88
|
@staticmethod
|
|
99
89
|
def _format_user_went_offline(user):
|
|
100
|
-
return
|
|
90
|
+
return f"{user.full_name} went OFFLINE."
|
|
101
91
|
|
|
102
92
|
@staticmethod
|
|
103
93
|
def _format_another_connection_error(e):
|
|
104
|
-
return
|
|
105
|
-
'Encountered a connection error which looks like the previous one: '
|
|
106
|
-
+ str(e)
|
|
107
|
-
)
|
|
94
|
+
return f"Encountered a connection error which looks like the previous one: {e}"
|
|
@@ -12,9 +12,9 @@ from . import backend
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Format(Enum):
|
|
15
|
-
CSV =
|
|
16
|
-
LOG =
|
|
17
|
-
NULL =
|
|
15
|
+
CSV = "csv"
|
|
16
|
+
LOG = "log"
|
|
17
|
+
NULL = "null"
|
|
18
18
|
|
|
19
19
|
def __str__(self):
|
|
20
20
|
return self.value
|
|
@@ -26,7 +26,7 @@ class Format(Enum):
|
|
|
26
26
|
return backend.log.Writer(fd)
|
|
27
27
|
if self is Format.NULL:
|
|
28
28
|
return backend.null.Writer()
|
|
29
|
-
raise NotImplementedError(
|
|
29
|
+
raise NotImplementedError(f"unsupported database format: {self}")
|
|
30
30
|
|
|
31
31
|
def open_output_file(self, path=None):
|
|
32
32
|
if self is Format.CSV:
|
|
@@ -35,30 +35,30 @@ class Format(Enum):
|
|
|
35
35
|
return self._open_output_log_file(path)
|
|
36
36
|
if self is Format.NULL:
|
|
37
37
|
return self._open_output_database_file(None)
|
|
38
|
-
raise NotImplementedError(
|
|
38
|
+
raise NotImplementedError(f"unsupported database format: {self}")
|
|
39
39
|
|
|
40
40
|
@staticmethod
|
|
41
41
|
def _open_output_log_file(path):
|
|
42
|
-
return io.open_output_text_file(path, mode=
|
|
42
|
+
return io.open_output_text_file(path, mode="a")
|
|
43
43
|
|
|
44
44
|
@staticmethod
|
|
45
45
|
def _open_output_database_file(path):
|
|
46
|
-
return io.open_output_text_file(path, mode=
|
|
46
|
+
return io.open_output_text_file(path, mode="x")
|
|
47
47
|
|
|
48
48
|
def create_reader(self, fd=sys.stdin):
|
|
49
49
|
if self is Format.CSV:
|
|
50
50
|
return backend.csv.Reader(fd)
|
|
51
51
|
if self is Format.LOG:
|
|
52
|
-
return NotImplementedError(
|
|
52
|
+
return NotImplementedError("cannot read from a log file")
|
|
53
53
|
if self is Format.NULL:
|
|
54
54
|
return backend.null.Reader()
|
|
55
|
-
raise NotImplementedError(
|
|
55
|
+
raise NotImplementedError(f"unsupported database format: {self}")
|
|
56
56
|
|
|
57
57
|
def open_input_file(self, path=None):
|
|
58
58
|
if self is Format.CSV:
|
|
59
59
|
return io.open_input_text_file(path)
|
|
60
60
|
if self is Format.LOG:
|
|
61
|
-
raise NotImplementedError(
|
|
61
|
+
raise NotImplementedError("cannot read from a log file")
|
|
62
62
|
if self is Format.NULL:
|
|
63
63
|
return io.open_input_text_file(None)
|
|
64
|
-
raise NotImplementedError(
|
|
64
|
+
raise NotImplementedError(f"unsupported database format: {self}")
|
|
@@ -73,7 +73,7 @@ class Record(MutableMapping):
|
|
|
73
73
|
if isinstance(field, UserField):
|
|
74
74
|
record[field] = user[field]
|
|
75
75
|
elif isinstance(field, LastSeenField):
|
|
76
|
-
record[field] = user.
|
|
76
|
+
record[field] = user.last_seen[field]
|
|
77
77
|
else:
|
|
78
78
|
assert False
|
|
79
79
|
return record
|
|
@@ -98,5 +98,5 @@ class Record(MutableMapping):
|
|
|
98
98
|
else:
|
|
99
99
|
assert False
|
|
100
100
|
if last_seen:
|
|
101
|
-
user.
|
|
101
|
+
user.last_seen = last_seen
|
|
102
102
|
return user
|
|
@@ -30,7 +30,7 @@ class Timestamp:
|
|
|
30
30
|
|
|
31
31
|
@staticmethod
|
|
32
32
|
def from_string(s):
|
|
33
|
-
return Timestamp(datetime.strptime(s,
|
|
33
|
+
return Timestamp(datetime.strptime(s, "%Y-%m-%dT%H:%M:%SZ"))
|
|
34
34
|
|
|
35
35
|
def __str__(self):
|
|
36
|
-
return self.dt.isoformat()
|
|
36
|
+
return f"{self.dt.isoformat()}Z"
|