vk-scripts 1.0.4__tar.gz → 1.0.5__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/.git-blame-ignore-revs +1 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/.github/workflows/ci.yml +18 -3
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/LICENSE.txt +1 -1
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/PKG-INFO +3 -3
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/pyproject.toml +9 -3
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/test/bin/main.sh +1 -1
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/test/bin/mutuals.sh +1 -1
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/test/bin/sessions.sh +1 -1
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/test/bin/status.sh +1 -1
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/test/bin/status_once.sh +1 -1
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/test/lib/test.sh +1 -1
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/api.py +23 -14
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/error.py +1 -1
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/last_seen.py +1 -1
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/mutuals.py +22 -12
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/platform.py +7 -11
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/tracking/__init__.py +5 -2
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/tracking/db/__init__.py +1 -1
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/tracking/db/backend/__init__.py +6 -2
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/tracking/db/backend/csv.py +1 -1
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/tracking/db/backend/log.py +12 -6
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/tracking/db/backend/null.py +1 -1
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/tracking/db/format.py +1 -1
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/tracking/db/meta.py +1 -1
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/tracking/db/record.py +1 -1
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/tracking/db/timestamp.py +1 -1
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/tracking/sessions.py +78 -44
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/tracking/status.py +55 -29
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/user.py +2 -2
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/utils/bar_chart.py +20 -17
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/utils/io.py +3 -5
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/version.py +3 -2
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk_scripts.egg-info/PKG-INFO +3 -3
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk_scripts.egg-info/SOURCES.txt +1 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/.gitattributes +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/.github/dependabot.yml +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/.gitignore +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/.pylintrc +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/README.md +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/docs/images/date.png +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/docs/images/hour.png +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/docs/images/user.png +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/docs/images/weekday.png +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/docs/mutuals.md +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/docs/sessions.md +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/docs/status.md +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/setup.cfg +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/test/share/test_db.csv +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/__init__.py +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk/utils/__init__.py +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk_scripts.egg-info/dependency_links.txt +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk_scripts.egg-info/entry_points.txt +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk_scripts.egg-info/requires.txt +0 -0
- {vk_scripts-1.0.4 → vk_scripts-1.0.5}/vk_scripts.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
93ad1eb3a63041a49882e6b7b6b0ef506d4fa6c7
|
|
@@ -6,8 +6,8 @@ on:
|
|
|
6
6
|
- 'docs/**'
|
|
7
7
|
- 'README.md'
|
|
8
8
|
schedule:
|
|
9
|
-
# Weekly, at
|
|
10
|
-
- cron: '
|
|
9
|
+
# Weekly, at 02:30 on Friday (somewhat randomly chosen).
|
|
10
|
+
- cron: '30 02 * * 5'
|
|
11
11
|
workflow_dispatch:
|
|
12
12
|
|
|
13
13
|
env:
|
|
@@ -15,6 +15,21 @@ env:
|
|
|
15
15
|
PIP_NO_PYTHON_VERSION_WARNING: 1
|
|
16
16
|
|
|
17
17
|
jobs:
|
|
18
|
+
lint:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
name: Linting
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout
|
|
23
|
+
uses: actions/checkout@v6
|
|
24
|
+
with:
|
|
25
|
+
fetch-depth: 0
|
|
26
|
+
- name: Set up Python
|
|
27
|
+
uses: actions/setup-python@v6
|
|
28
|
+
with:
|
|
29
|
+
python-version: '3.x'
|
|
30
|
+
- name: Run black
|
|
31
|
+
uses: psf/black@stable
|
|
32
|
+
|
|
18
33
|
test:
|
|
19
34
|
strategy:
|
|
20
35
|
matrix:
|
|
@@ -43,7 +58,7 @@ jobs:
|
|
|
43
58
|
run: ./test/bin/main.sh
|
|
44
59
|
|
|
45
60
|
publish_pypi:
|
|
46
|
-
needs: [test]
|
|
61
|
+
needs: [lint, test]
|
|
47
62
|
runs-on: ubuntu-latest
|
|
48
63
|
name: Publish
|
|
49
64
|
steps:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2015 Egor Tensin <
|
|
3
|
+
Copyright (c) 2015 Egor Tensin <egor@tensin.name>
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vk_scripts
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.5
|
|
4
4
|
Summary: Scripts to stalk people on VK
|
|
5
|
-
Author-email: Egor Tensin <
|
|
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
|
-
Classifier: Development Status ::
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
10
|
Requires-Python: >=3.4
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE.txt
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
|
|
2
|
+
# The SPDX license identifiers are available from 77.0.3:
|
|
3
|
+
# https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#license-and-license-files
|
|
4
|
+
requires = ["setuptools >= 77.0.3", "setuptools-scm"]
|
|
3
5
|
build-backend = "setuptools.build_meta"
|
|
4
6
|
|
|
5
7
|
[project]
|
|
6
8
|
name = "vk_scripts"
|
|
7
9
|
description = "Scripts to stalk people on VK"
|
|
8
10
|
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE.txt"]
|
|
9
12
|
dynamic = ["version"]
|
|
10
|
-
authors = [{name = "Egor Tensin", email = "
|
|
13
|
+
authors = [{name = "Egor Tensin", email = "egor@tensin.name"}]
|
|
11
14
|
readme = "README.md"
|
|
12
15
|
requires-python = ">=3.4"
|
|
13
16
|
|
|
@@ -17,7 +20,7 @@ dependencies = [
|
|
|
17
20
|
]
|
|
18
21
|
|
|
19
22
|
classifiers = [
|
|
20
|
-
"Development Status ::
|
|
23
|
+
"Development Status :: 5 - Production/Stable",
|
|
21
24
|
]
|
|
22
25
|
|
|
23
26
|
[project.urls]
|
|
@@ -30,3 +33,6 @@ vk-status = "vk.tracking.status:main"
|
|
|
30
33
|
vk-mutuals = "vk.mutuals:main"
|
|
31
34
|
|
|
32
35
|
[tool.setuptools_scm]
|
|
36
|
+
|
|
37
|
+
[tool.black]
|
|
38
|
+
skip-string-normalization = true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
-
# Copyright (c) 2019 Egor Tensin <
|
|
3
|
+
# Copyright (c) 2019 Egor Tensin <egor@tensin.name>
|
|
4
4
|
# This file is part of the "VK scripts" project.
|
|
5
5
|
# For details, see https://github.com/egor-tensin/vk-scripts.
|
|
6
6
|
# Distributed under the MIT License.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
-
# Copyright (c) 2019 Egor Tensin <
|
|
3
|
+
# Copyright (c) 2019 Egor Tensin <egor@tensin.name>
|
|
4
4
|
# This file is part of the "VK scripts" project.
|
|
5
5
|
# For details, see https://github.com/egor-tensin/vk-scripts.
|
|
6
6
|
# Distributed under the MIT License.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
-
# Copyright (c) 2019 Egor Tensin <
|
|
3
|
+
# Copyright (c) 2019 Egor Tensin <egor@tensin.name>
|
|
4
4
|
# This file is part of the "VK scripts" project.
|
|
5
5
|
# For details, see https://github.com/egor-tensin/vk-scripts.
|
|
6
6
|
# Distributed under the MIT License.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
-
# Copyright (c) 2019 Egor Tensin <
|
|
3
|
+
# Copyright (c) 2019 Egor Tensin <egor@tensin.name>
|
|
4
4
|
# This file is part of the "VK scripts" project.
|
|
5
5
|
# For details, see https://github.com/egor-tensin/vk-scripts.
|
|
6
6
|
# Distributed under the MIT License.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
-
# Copyright (c) 2019 Egor Tensin <
|
|
3
|
+
# Copyright (c) 2019 Egor Tensin <egor@tensin.name>
|
|
4
4
|
# This file is part of the "VK scripts" project.
|
|
5
5
|
# For details, see https://github.com/egor-tensin/vk-scripts.
|
|
6
6
|
# Distributed under the MIT License.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
-
# Copyright (c) 2020 Egor Tensin <
|
|
3
|
+
# Copyright (c) 2020 Egor Tensin <egor@tensin.name>
|
|
4
4
|
# This file is part of the "VK scripts" project.
|
|
5
5
|
# For details, see https://github.com/egor-tensin/vk-scripts.
|
|
6
6
|
# Distributed under the MIT License.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2015 Egor Tensin <
|
|
1
|
+
# Copyright (c) 2015 Egor Tensin <egor@tensin.name>
|
|
2
2
|
# This file is part of the "VK scripts" project.
|
|
3
3
|
# For details, see https://github.com/egor-tensin/vk-scripts.
|
|
4
4
|
# Distributed under the MIT License.
|
|
@@ -26,9 +26,15 @@ def _filter_empty_params(params, empty_params=False):
|
|
|
26
26
|
if empty_params:
|
|
27
27
|
return params
|
|
28
28
|
if isinstance(params, Mapping):
|
|
29
|
-
return {
|
|
29
|
+
return {
|
|
30
|
+
name: value
|
|
31
|
+
for name, value in params.items()
|
|
32
|
+
if not _is_empty_param_value(value)
|
|
33
|
+
}
|
|
30
34
|
if isinstance(params, Iterable):
|
|
31
|
-
return [
|
|
35
|
+
return [
|
|
36
|
+
(name, value) for name, value in params if not _is_empty_param_value(value)
|
|
37
|
+
]
|
|
32
38
|
raise TypeError()
|
|
33
39
|
|
|
34
40
|
|
|
@@ -64,6 +70,7 @@ ACCESS_TOKEN = '9722cef09722cef09722cef071974b8cbe997229722cef0cbabfd816916af6c7
|
|
|
64
70
|
|
|
65
71
|
|
|
66
72
|
class Version(Enum):
|
|
73
|
+
# https://dev.vk.com/en/reference/versions
|
|
67
74
|
V5_199 = '5.199'
|
|
68
75
|
DEFAULT = V5_199
|
|
69
76
|
|
|
@@ -115,11 +122,11 @@ class API:
|
|
|
115
122
|
|
|
116
123
|
def _call_method(self, method, **params):
|
|
117
124
|
url = self._build_method_url(method, **params)
|
|
118
|
-
#print(url)
|
|
125
|
+
# print(url)
|
|
119
126
|
try:
|
|
120
127
|
with urlopen(url) as response:
|
|
121
128
|
response = json.loads(response.read().decode())
|
|
122
|
-
#print(response)
|
|
129
|
+
# print(response)
|
|
123
130
|
if 'response' not in response:
|
|
124
131
|
raise vk.error.InvalidAPIResponseError(response)
|
|
125
132
|
return response['response']
|
|
@@ -134,17 +141,19 @@ class API:
|
|
|
134
141
|
return [user for user in user_list if not user.is_deactivated()]
|
|
135
142
|
|
|
136
143
|
def users_get(self, user_ids, fields=(), deactivated_users=True):
|
|
137
|
-
return self._filter_response_with_users(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
return self._filter_response_with_users(
|
|
145
|
+
self._call_method(
|
|
146
|
+
Method.USERS_GET,
|
|
147
|
+
user_ids=_join_param_values(user_ids),
|
|
148
|
+
fields=_join_param_values(fields),
|
|
149
|
+
),
|
|
150
|
+
deactivated_users,
|
|
151
|
+
)
|
|
141
152
|
|
|
142
153
|
def friends_get(self, user_id, fields=(), deactivated_users=True):
|
|
143
154
|
response = self._call_method(
|
|
144
|
-
Method.FRIENDS_GET,
|
|
145
|
-
|
|
146
|
-
fields=_join_param_values(fields))
|
|
155
|
+
Method.FRIENDS_GET, user_id=user_id, fields=_join_param_values(fields)
|
|
156
|
+
)
|
|
147
157
|
if 'items' not in response:
|
|
148
158
|
raise vk.error.InvalidAPIResponseError(response)
|
|
149
|
-
return self._filter_response_with_users(response['items'],
|
|
150
|
-
deactivated_users)
|
|
159
|
+
return self._filter_response_with_users(response['items'], deactivated_users)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2015 Egor Tensin <
|
|
1
|
+
# Copyright (c) 2015 Egor Tensin <egor@tensin.name>
|
|
2
2
|
# This file is part of the "VK scripts" project.
|
|
3
3
|
# For details, see https://github.com/egor-tensin/vk-scripts.
|
|
4
4
|
# Distributed under the MIT License.
|
|
@@ -14,7 +14,6 @@ from vk.user import UserField
|
|
|
14
14
|
from vk.utils import io
|
|
15
15
|
import vk.version
|
|
16
16
|
|
|
17
|
-
|
|
18
17
|
_OUTPUT_USER_FIELDS = UserField.UID, UserField.FIRST_NAME, UserField.LAST_NAME
|
|
19
18
|
|
|
20
19
|
|
|
@@ -83,19 +82,30 @@ def _parse_args(args=None):
|
|
|
83
82
|
args = sys.argv[1:]
|
|
84
83
|
|
|
85
84
|
parser = argparse.ArgumentParser(
|
|
86
|
-
description='Learn who your ex and her new boyfriend are both friends with.'
|
|
85
|
+
description='Learn who your ex and her new boyfriend are both friends with.'
|
|
86
|
+
)
|
|
87
87
|
|
|
88
88
|
vk.version.add_to_arg_parser(parser)
|
|
89
89
|
|
|
90
|
-
parser.add_argument(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
'uids', metavar='UID', nargs='+', help='user IDs or "screen names"'
|
|
92
|
+
)
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
'-f',
|
|
95
|
+
'--format',
|
|
96
|
+
dest='out_fmt',
|
|
97
|
+
type=_parse_output_format,
|
|
98
|
+
default=OutputFormat.CSV,
|
|
99
|
+
choices=OutputFormat,
|
|
100
|
+
help='specify output format',
|
|
101
|
+
)
|
|
102
|
+
parser.add_argument(
|
|
103
|
+
'-o',
|
|
104
|
+
'--output',
|
|
105
|
+
metavar='PATH',
|
|
106
|
+
dest='out_path',
|
|
107
|
+
help='set output file path (standard output by default)',
|
|
108
|
+
)
|
|
99
109
|
|
|
100
110
|
return parser.parse_args(args)
|
|
101
111
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2016 Egor Tensin <
|
|
1
|
+
# Copyright (c) 2016 Egor Tensin <egor@tensin.name>
|
|
2
2
|
# This file is part of the "VK scripts" project.
|
|
3
3
|
# For details, see https://github.com/egor-tensin/vk-scripts.
|
|
4
4
|
# Distributed under the MIT License.
|
|
@@ -8,14 +8,14 @@ import re
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class Platform(Enum):
|
|
11
|
+
# https://dev.vk.com/en/reference/objects/user#last_seen
|
|
11
12
|
MOBILE = 1
|
|
12
13
|
IPHONE = 2
|
|
13
14
|
IPAD = 3
|
|
14
15
|
ANDROID = 4
|
|
15
16
|
WINDOWS_PHONE = 5
|
|
16
|
-
|
|
17
|
+
WINDOWS10 = 6
|
|
17
18
|
WEB = 7
|
|
18
|
-
VK_MOBILE = 8
|
|
19
19
|
|
|
20
20
|
@staticmethod
|
|
21
21
|
def from_string(s):
|
|
@@ -29,16 +29,13 @@ class Platform(Enum):
|
|
|
29
29
|
m = re.search(r'\w', s)
|
|
30
30
|
if m is None:
|
|
31
31
|
return s
|
|
32
|
-
return s[:m.start()] + m.group().upper() + s[m.end():]
|
|
32
|
+
return s[: m.start()] + m.group().upper() + s[m.end() :]
|
|
33
33
|
|
|
34
34
|
def get_descr_header(self):
|
|
35
35
|
return self._capitalize_first_letter(_PLATFORM_DESCRIPTIONS[self])
|
|
36
36
|
|
|
37
37
|
def get_descr_text(self):
|
|
38
38
|
s = _PLATFORM_DESCRIPTIONS[self]
|
|
39
|
-
if self == Platform.VK_MOBILE:
|
|
40
|
-
return s
|
|
41
|
-
s = s.replace('unrecognized', 'an unrecognized')
|
|
42
39
|
return 'the ' + s
|
|
43
40
|
|
|
44
41
|
def get_descr_text_capitalized(self):
|
|
@@ -46,12 +43,11 @@ class Platform(Enum):
|
|
|
46
43
|
|
|
47
44
|
|
|
48
45
|
_PLATFORM_DESCRIPTIONS = {
|
|
49
|
-
Platform.MOBILE: '"mobile" web version (or unrecognized mobile app)',
|
|
46
|
+
Platform.MOBILE: '"mobile" web version (or an unrecognized mobile app)',
|
|
50
47
|
Platform.IPHONE: 'official iPhone app',
|
|
51
48
|
Platform.IPAD: 'official iPad app',
|
|
52
49
|
Platform.ANDROID: 'official Android app',
|
|
53
50
|
Platform.WINDOWS_PHONE: 'official Windows Phone app',
|
|
54
|
-
Platform.
|
|
55
|
-
Platform.WEB: 'web version (or unrecognized app)',
|
|
56
|
-
Platform.VK_MOBILE: 'VK Mobile',
|
|
51
|
+
Platform.WINDOWS10: 'official Windows 10 app',
|
|
52
|
+
Platform.WEB: 'web version (or an unrecognized app)',
|
|
57
53
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
# Copyright 2016 Egor Tensin <
|
|
1
|
+
# Copyright 2016 Egor Tensin <egor@tensin.name>
|
|
2
2
|
# This file is licensed under the terms of the MIT License.
|
|
3
3
|
# See LICENSE.txt for details.
|
|
4
4
|
|
|
5
|
-
__all__ =
|
|
5
|
+
__all__ = (
|
|
6
|
+
'sessions',
|
|
7
|
+
'status',
|
|
8
|
+
)
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
# Copyright (c) 2016 Egor Tensin <
|
|
1
|
+
# Copyright (c) 2016 Egor Tensin <egor@tensin.name>
|
|
2
2
|
# This file is part of the "VK scripts" project.
|
|
3
3
|
# For details, see https://github.com/egor-tensin/vk-scripts.
|
|
4
4
|
# Distributed under the MIT License.
|
|
5
5
|
|
|
6
6
|
from . import csv, log, null
|
|
7
7
|
|
|
8
|
-
__all__ =
|
|
8
|
+
__all__ = (
|
|
9
|
+
'csv',
|
|
10
|
+
'log',
|
|
11
|
+
'null',
|
|
12
|
+
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2016 Egor Tensin <
|
|
1
|
+
# Copyright (c) 2016 Egor Tensin <egor@tensin.name>
|
|
2
2
|
# This file is part of the "VK scripts" project.
|
|
3
3
|
# For details, see https://github.com/egor-tensin/vk-scripts.
|
|
4
4
|
# Distributed under the MIT License.
|
|
@@ -13,9 +13,11 @@ class Writer(meta.Writer):
|
|
|
13
13
|
self._logger = logging.getLogger(__file__)
|
|
14
14
|
self._logger.setLevel(logging.INFO)
|
|
15
15
|
handler = logging.StreamHandler(fd)
|
|
16
|
-
handler.setFormatter(
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
handler.setFormatter(
|
|
17
|
+
logging.Formatter(
|
|
18
|
+
fmt='[%(asctime)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S%z'
|
|
19
|
+
)
|
|
20
|
+
)
|
|
19
21
|
self._logger.addHandler(handler)
|
|
20
22
|
|
|
21
23
|
self._reset_last_notification()
|
|
@@ -86,7 +88,8 @@ class Writer(meta.Writer):
|
|
|
86
88
|
return '{} was last seen at {} using {}.'.format(
|
|
87
89
|
Writer._format_user(user),
|
|
88
90
|
user.get_last_seen_time_local(),
|
|
89
|
-
user.get_last_seen_platform().get_descr_text()
|
|
91
|
+
user.get_last_seen_platform().get_descr_text(),
|
|
92
|
+
)
|
|
90
93
|
|
|
91
94
|
@staticmethod
|
|
92
95
|
def _format_user_went_online(user):
|
|
@@ -98,4 +101,7 @@ class Writer(meta.Writer):
|
|
|
98
101
|
|
|
99
102
|
@staticmethod
|
|
100
103
|
def _format_another_connection_error(e):
|
|
101
|
-
return
|
|
104
|
+
return (
|
|
105
|
+
'Encountered a connection error which looks like the previous one: '
|
|
106
|
+
+ str(e)
|
|
107
|
+
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2016 Egor Tensin <
|
|
1
|
+
# Copyright (c) 2016 Egor Tensin <egor@tensin.name>
|
|
2
2
|
# This file is part of the "VK scripts" project.
|
|
3
3
|
# For details, see https://github.com/egor-tensin/vk-scripts.
|
|
4
4
|
# Distributed under the MIT License.
|
|
@@ -339,10 +339,12 @@ class OutputSinkPlot(OutputSinkOnlineSessions):
|
|
|
339
339
|
|
|
340
340
|
@staticmethod
|
|
341
341
|
def _extract_values(durations):
|
|
342
|
-
return (
|
|
342
|
+
return (
|
|
343
|
+
OutputSinkPlot._duration_to_seconds(duration)
|
|
344
|
+
for duration in durations.values()
|
|
345
|
+
)
|
|
343
346
|
|
|
344
|
-
def process_database(
|
|
345
|
-
self, group_by, db_reader, time_from=None, time_to=None):
|
|
347
|
+
def process_database(self, group_by, db_reader, time_from=None, time_to=None):
|
|
346
348
|
|
|
347
349
|
durations = group_by.group(db_reader, time_from, time_to)
|
|
348
350
|
|
|
@@ -350,8 +352,9 @@ class OutputSinkPlot(OutputSinkOnlineSessions):
|
|
|
350
352
|
bar_chart.set_title(OutputSinkPlot.TITLE)
|
|
351
353
|
bar_chart.enable_grid_for_values()
|
|
352
354
|
bar_chart.only_integer_values()
|
|
353
|
-
bar_chart.set_property(
|
|
354
|
-
|
|
355
|
+
bar_chart.set_property(
|
|
356
|
+
bar_chart.get_values_labels(), fontsize='small', rotation=30
|
|
357
|
+
)
|
|
355
358
|
bar_chart.set_value_label_formatter(self._format_duration)
|
|
356
359
|
|
|
357
360
|
labels = tuple(self._extract_labels(group_by, durations))
|
|
@@ -363,9 +366,8 @@ class OutputSinkPlot(OutputSinkOnlineSessions):
|
|
|
363
366
|
else:
|
|
364
367
|
bar_height = bar_chart.THICK_BAR_HEIGHT
|
|
365
368
|
|
|
366
|
-
bars = bar_chart.plot_bars(
|
|
367
|
-
|
|
368
|
-
bar_chart.set_property(bars, alpha=.33)
|
|
369
|
+
bars = bar_chart.plot_bars(labels, durations, bar_height=bar_height)
|
|
370
|
+
bar_chart.set_property(bars, alpha=0.33)
|
|
369
371
|
|
|
370
372
|
if self._fd is sys.stdout:
|
|
371
373
|
bar_chart.show()
|
|
@@ -426,8 +428,7 @@ def _parse_date_range_limit(s):
|
|
|
426
428
|
return dt.replace(tzinfo=timezone.utc)
|
|
427
429
|
except ValueError:
|
|
428
430
|
msg = 'invalid date range limit (must be in the \'{}\' format): {}'
|
|
429
|
-
raise argparse.ArgumentTypeError(
|
|
430
|
-
msg.format(_DATE_RANGE_LIMIT_FORMAT, s))
|
|
431
|
+
raise argparse.ArgumentTypeError(msg.format(_DATE_RANGE_LIMIT_FORMAT, s))
|
|
431
432
|
|
|
432
433
|
|
|
433
434
|
def _parse_args(args=None):
|
|
@@ -435,44 +436,78 @@ def _parse_args(args=None):
|
|
|
435
436
|
args = sys.argv[1:]
|
|
436
437
|
|
|
437
438
|
parser = argparse.ArgumentParser(
|
|
438
|
-
description='View/visualize the amount of time people spend online.'
|
|
439
|
+
description='View/visualize the amount of time people spend online.'
|
|
440
|
+
)
|
|
439
441
|
|
|
440
442
|
vk.version.add_to_arg_parser(parser)
|
|
441
443
|
|
|
442
|
-
parser.add_argument(
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
444
|
+
parser.add_argument(
|
|
445
|
+
'db_path',
|
|
446
|
+
metavar='input',
|
|
447
|
+
nargs='?',
|
|
448
|
+
help='database file path (standard input by default)',
|
|
449
|
+
)
|
|
450
|
+
parser.add_argument(
|
|
451
|
+
'out_path',
|
|
452
|
+
metavar='output',
|
|
453
|
+
nargs='?',
|
|
454
|
+
help='output file path (standard output by default)',
|
|
455
|
+
)
|
|
456
|
+
parser.add_argument(
|
|
457
|
+
'-g',
|
|
458
|
+
'--group-by',
|
|
459
|
+
type=_parse_group_by,
|
|
460
|
+
choices=GroupBy,
|
|
461
|
+
default=GroupBy.USER,
|
|
462
|
+
help='group online sessions by user/date/etc.',
|
|
463
|
+
)
|
|
464
|
+
parser.add_argument(
|
|
465
|
+
'-i',
|
|
466
|
+
'--input-format',
|
|
467
|
+
dest='db_fmt',
|
|
468
|
+
type=_parse_database_format,
|
|
469
|
+
default=DatabaseFormat.CSV,
|
|
470
|
+
choices=DatabaseFormat,
|
|
471
|
+
help='specify database format',
|
|
472
|
+
)
|
|
473
|
+
parser.add_argument(
|
|
474
|
+
'-o',
|
|
475
|
+
'--output-format',
|
|
476
|
+
dest='out_fmt',
|
|
477
|
+
type=_parse_output_format,
|
|
478
|
+
choices=OutputFormat,
|
|
479
|
+
default=OutputFormat.CSV,
|
|
480
|
+
help='specify output format',
|
|
481
|
+
)
|
|
482
|
+
parser.add_argument(
|
|
483
|
+
'-a',
|
|
484
|
+
'--from',
|
|
485
|
+
dest='time_from',
|
|
486
|
+
type=_parse_date_range_limit,
|
|
487
|
+
default=None,
|
|
488
|
+
help='discard online activity prior to this moment',
|
|
489
|
+
)
|
|
490
|
+
parser.add_argument(
|
|
491
|
+
'-b',
|
|
492
|
+
'--to',
|
|
493
|
+
dest='time_to',
|
|
494
|
+
type=_parse_date_range_limit,
|
|
495
|
+
default=None,
|
|
496
|
+
help='discard online activity after this moment',
|
|
497
|
+
)
|
|
467
498
|
|
|
468
499
|
return parser.parse_args(args)
|
|
469
500
|
|
|
470
501
|
|
|
471
502
|
def process_online_sessions(
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
503
|
+
db_path=None,
|
|
504
|
+
db_fmt=DatabaseFormat.CSV,
|
|
505
|
+
out_path=None,
|
|
506
|
+
out_fmt=OutputFormat.CSV,
|
|
507
|
+
group_by=GroupBy.USER,
|
|
508
|
+
time_from=None,
|
|
509
|
+
time_to=None,
|
|
510
|
+
):
|
|
476
511
|
|
|
477
512
|
if time_from is not None and time_to is not None:
|
|
478
513
|
if time_from > time_to:
|
|
@@ -483,9 +518,8 @@ def process_online_sessions(
|
|
|
483
518
|
with out_fmt.open_file(out_path) as out_fd:
|
|
484
519
|
out_sink = out_fmt.create_sink(out_fd)
|
|
485
520
|
out_sink.process_database(
|
|
486
|
-
group_by, db_reader,
|
|
487
|
-
|
|
488
|
-
time_to=time_to)
|
|
521
|
+
group_by, db_reader, time_from=time_from, time_to=time_to
|
|
522
|
+
)
|
|
489
523
|
|
|
490
524
|
|
|
491
525
|
def main(args=None):
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2016 Egor Tensin <
|
|
1
|
+
# Copyright (c) 2016 Egor Tensin <egor@tensin.name>
|
|
2
2
|
# This file is part of the "VK scripts" project.
|
|
3
3
|
# For details, see https://github.com/egor-tensin/vk-scripts.
|
|
4
4
|
# Distributed under the MIT License.
|
|
@@ -52,11 +52,16 @@ class StatusTracker:
|
|
|
52
52
|
if not isinstance(fn, Callable):
|
|
53
53
|
raise TypeError()
|
|
54
54
|
|
|
55
|
-
_USER_FIELDS =
|
|
55
|
+
_USER_FIELDS = (
|
|
56
|
+
UserField.DOMAIN,
|
|
57
|
+
UserField.ONLINE,
|
|
58
|
+
UserField.LAST_SEEN,
|
|
59
|
+
)
|
|
56
60
|
|
|
57
61
|
def _query_status(self, uids):
|
|
58
|
-
user_list = self._api.users_get(
|
|
59
|
-
|
|
62
|
+
user_list = self._api.users_get(
|
|
63
|
+
uids, self._USER_FIELDS, deactivated_users=False
|
|
64
|
+
)
|
|
60
65
|
return {user.get_uid(): user for user in user_list}
|
|
61
66
|
|
|
62
67
|
def _notify_status(self, user):
|
|
@@ -128,13 +133,11 @@ class StatusTracker:
|
|
|
128
133
|
def _handle_sigint():
|
|
129
134
|
# Python doesn't raise KeyboardInterrupt in case a real SIGINT is sent
|
|
130
135
|
# from outside, surprisingly.
|
|
131
|
-
return StatusTracker._handle_signal(signal.SIGINT,
|
|
132
|
-
StatusTracker._stop_looping)
|
|
136
|
+
return StatusTracker._handle_signal(signal.SIGINT, StatusTracker._stop_looping)
|
|
133
137
|
|
|
134
138
|
@staticmethod
|
|
135
139
|
def _handle_sigterm():
|
|
136
|
-
return StatusTracker._handle_signal(signal.SIGTERM,
|
|
137
|
-
StatusTracker._stop_looping)
|
|
140
|
+
return StatusTracker._handle_signal(signal.SIGTERM, StatusTracker._stop_looping)
|
|
138
141
|
|
|
139
142
|
def loop(self, uids):
|
|
140
143
|
with self._handle_sigint(), self._handle_sigterm():
|
|
@@ -169,35 +172,58 @@ def _parse_args(args=None):
|
|
|
169
172
|
if args is None:
|
|
170
173
|
args = sys.argv[1:]
|
|
171
174
|
|
|
172
|
-
parser = argparse.ArgumentParser(
|
|
173
|
-
description='Track when people go online/offline.')
|
|
175
|
+
parser = argparse.ArgumentParser(description='Track when people go online/offline.')
|
|
174
176
|
|
|
175
177
|
vk.version.add_to_arg_parser(parser)
|
|
176
178
|
|
|
177
|
-
parser.add_argument(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
179
|
+
parser.add_argument(
|
|
180
|
+
'uids', metavar='UID', nargs='+', help='user IDs or "screen names"'
|
|
181
|
+
)
|
|
182
|
+
parser.add_argument(
|
|
183
|
+
'-t',
|
|
184
|
+
'--timeout',
|
|
185
|
+
metavar='SECONDS',
|
|
186
|
+
type=_parse_positive_integer,
|
|
187
|
+
default=DEFAULT_TIMEOUT,
|
|
188
|
+
help='set refresh interval',
|
|
189
|
+
)
|
|
190
|
+
parser.add_argument(
|
|
191
|
+
'-O',
|
|
192
|
+
'--only-once',
|
|
193
|
+
action='store_true',
|
|
194
|
+
help='query the status only once and exit',
|
|
195
|
+
)
|
|
196
|
+
parser.add_argument(
|
|
197
|
+
'-l',
|
|
198
|
+
'--log',
|
|
199
|
+
metavar='PATH',
|
|
200
|
+
dest='log_path',
|
|
201
|
+
help='set log file path (standard output by default)',
|
|
202
|
+
)
|
|
203
|
+
parser.add_argument(
|
|
204
|
+
'-f',
|
|
205
|
+
'--format',
|
|
206
|
+
dest='db_fmt',
|
|
207
|
+
type=_parse_database_format,
|
|
208
|
+
choices=DatabaseFormat,
|
|
209
|
+
default=DEFAULT_DB_FORMAT,
|
|
210
|
+
help='specify database format',
|
|
211
|
+
)
|
|
212
|
+
parser.add_argument(
|
|
213
|
+
'-o', '--output', metavar='PATH', dest='db_path', help='set database file path'
|
|
214
|
+
)
|
|
194
215
|
|
|
195
216
|
return parser.parse_args(args)
|
|
196
217
|
|
|
197
218
|
|
|
198
219
|
def track_status(
|
|
199
|
-
|
|
200
|
-
|
|
220
|
+
uids,
|
|
221
|
+
timeout=DEFAULT_TIMEOUT,
|
|
222
|
+
log_path=None,
|
|
223
|
+
db_path=None,
|
|
224
|
+
db_fmt=DEFAULT_DB_FORMAT,
|
|
225
|
+
only_once=False,
|
|
226
|
+
):
|
|
201
227
|
|
|
202
228
|
api = API()
|
|
203
229
|
tracker = StatusTracker(api, timeout)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2016 Egor Tensin <
|
|
1
|
+
# Copyright (c) 2016 Egor Tensin <egor@tensin.name>
|
|
2
2
|
# This file is part of the "VK scripts" project.
|
|
3
3
|
# For details, see https://github.com/egor-tensin/vk-scripts.
|
|
4
4
|
# Distributed under the MIT License.
|
|
@@ -79,7 +79,7 @@ class User(Hashable, MutableMapping):
|
|
|
79
79
|
|
|
80
80
|
def __eq__(self, other):
|
|
81
81
|
return self.get_uid() == other.get_uid()
|
|
82
|
-
#return self._fields == other._fields
|
|
82
|
+
# return self._fields == other._fields
|
|
83
83
|
|
|
84
84
|
def __hash__(self):
|
|
85
85
|
return hash(self.get_uid())
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2017 Egor Tensin <
|
|
1
|
+
# Copyright (c) 2017 Egor Tensin <egor@tensin.name>
|
|
2
2
|
# This file is part of the "VK scripts" project.
|
|
3
3
|
# For details, see https://github.com/egor-tensin/vk-scripts.
|
|
4
4
|
# Distributed under the MIT License.
|
|
@@ -9,7 +9,7 @@ import numpy as np
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class BarChartBuilder:
|
|
12
|
-
_BAR_HEIGHT = .5
|
|
12
|
+
_BAR_HEIGHT = 0.5
|
|
13
13
|
|
|
14
14
|
THICK_BAR_HEIGHT = _BAR_HEIGHT
|
|
15
15
|
THIN_BAR_HEIGHT = THICK_BAR_HEIGHT / 2
|
|
@@ -112,12 +112,11 @@ class BarChartBuilder:
|
|
|
112
112
|
|
|
113
113
|
self._get_categories_axis().set_ticklabels(categories)
|
|
114
114
|
|
|
115
|
-
bars = self._ax.barh(bar_offsets, values, align='center',
|
|
116
|
-
height=bar_height)
|
|
115
|
+
bars = self._ax.barh(bar_offsets, values, align='center', height=bar_height)
|
|
117
116
|
|
|
118
117
|
if min(values) >= 0:
|
|
119
118
|
self.set_values_axis_limits(start=0)
|
|
120
|
-
if np.isclose(max(values), 0.):
|
|
119
|
+
if np.isclose(max(values), 0.0):
|
|
121
120
|
self.set_values_axis_limits(end=self._DEFAULT_VALUES_AXIS_MAX)
|
|
122
121
|
elif max(values) < 0:
|
|
123
122
|
self.set_values_axis_limits(end=0)
|
|
@@ -134,23 +133,27 @@ class BarChartBuilder:
|
|
|
134
133
|
|
|
135
134
|
if __name__ == '__main__':
|
|
136
135
|
import argparse
|
|
136
|
+
|
|
137
137
|
parser = argparse.ArgumentParser()
|
|
138
138
|
|
|
139
|
-
parser.add_argument('--categories', nargs='*', metavar='LABEL',
|
|
140
|
-
|
|
141
|
-
parser.add_argument('--values', nargs='*', metavar='N',
|
|
142
|
-
default=[], type=float)
|
|
139
|
+
parser.add_argument('--categories', nargs='*', metavar='LABEL', default=[])
|
|
140
|
+
parser.add_argument('--values', nargs='*', metavar='N', default=[], type=float)
|
|
143
141
|
|
|
144
142
|
parser.add_argument('--output', '-o', help='set output file path')
|
|
145
143
|
|
|
146
|
-
parser.add_argument(
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
parser.add_argument(
|
|
145
|
+
'--align-middle',
|
|
146
|
+
action='store_true',
|
|
147
|
+
dest='labels_align_middle',
|
|
148
|
+
help='align labels to the middle of the bars',
|
|
149
|
+
)
|
|
149
150
|
|
|
150
|
-
parser.add_argument(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
parser.add_argument(
|
|
152
|
+
'--integer-values', action='store_true', dest='only_integer_values'
|
|
153
|
+
)
|
|
154
|
+
parser.add_argument(
|
|
155
|
+
'--any-values', action='store_false', dest='only_integer_values'
|
|
156
|
+
)
|
|
154
157
|
|
|
155
158
|
parser.add_argument('--grid-categories', action='store_true')
|
|
156
159
|
parser.add_argument('--grid-values', action='store_true')
|
|
@@ -160,7 +163,7 @@ if __name__ == '__main__':
|
|
|
160
163
|
if len(args.categories) < len(args.values):
|
|
161
164
|
parser.error('too many bar values')
|
|
162
165
|
if len(args.categories) > len(args.values):
|
|
163
|
-
args.values.extend([0.] * (len(args.categories) - len(args.values)))
|
|
166
|
+
args.values.extend([0.0] * (len(args.categories) - len(args.values)))
|
|
164
167
|
|
|
165
168
|
builder = BarChartBuilder(labels_align_middle=args.labels_align_middle)
|
|
166
169
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2017 Egor Tensin <
|
|
1
|
+
# Copyright (c) 2017 Egor Tensin <egor@tensin.name>
|
|
2
2
|
# This file is part of the "VK scripts" project.
|
|
3
3
|
# For details, see https://github.com/egor-tensin/vk-scripts.
|
|
4
4
|
# Distributed under the MIT License.
|
|
@@ -59,13 +59,11 @@ _DEFAULT_ENCODING = 'utf-8'
|
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
def open_input_text_file(path=None):
|
|
62
|
-
return _open_file(path, default=sys.stdin, mode='r',
|
|
63
|
-
encoding=_DEFAULT_ENCODING)
|
|
62
|
+
return _open_file(path, default=sys.stdin, mode='r', encoding=_DEFAULT_ENCODING)
|
|
64
63
|
|
|
65
64
|
|
|
66
65
|
def open_output_text_file(path=None, mode='w'):
|
|
67
|
-
return _open_file(path, default=sys.stdout, mode=mode,
|
|
68
|
-
encoding=_DEFAULT_ENCODING)
|
|
66
|
+
return _open_file(path, default=sys.stdout, mode=mode, encoding=_DEFAULT_ENCODING)
|
|
69
67
|
|
|
70
68
|
|
|
71
69
|
def open_output_binary_file(path=None):
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vk_scripts
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.5
|
|
4
4
|
Summary: Scripts to stalk people on VK
|
|
5
|
-
Author-email: Egor Tensin <
|
|
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
|
-
Classifier: Development Status ::
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
10
|
Requires-Python: >=3.4
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|