aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/vk/utils/tracking
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--vk/utils/tracking/__init__.py8
-rw-r--r--vk/utils/tracking/db/__init__.py0
-rw-r--r--vk/utils/tracking/db/record.py27
-rw-r--r--vk/utils/tracking/db/writer/__init__.py5
-rw-r--r--vk/utils/tracking/db/writer/csv.py40
-rw-r--r--vk/utils/tracking/logger.py60
-rw-r--r--vk/utils/tracking/status_tracker.py94
7 files changed, 234 insertions, 0 deletions
diff --git a/vk/utils/tracking/__init__.py b/vk/utils/tracking/__init__.py
new file mode 100644
index 0000000..641d4c8
--- /dev/null
+++ b/vk/utils/tracking/__init__.py
@@ -0,0 +1,8 @@
+# Copyright 2016 Egor Tensin <Egor.Tensin@gmail.com>
+# This file is licensed under the terms of the MIT License.
+# See LICENSE.txt for details.
+
+from .logger import Logger
+from .status_tracker import StatusTracker
+
+__all__ = 'logger', 'status_tracker',
diff --git a/vk/utils/tracking/db/__init__.py b/vk/utils/tracking/db/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/vk/utils/tracking/db/__init__.py
diff --git a/vk/utils/tracking/db/record.py b/vk/utils/tracking/db/record.py
new file mode 100644
index 0000000..0a1a687
--- /dev/null
+++ b/vk/utils/tracking/db/record.py
@@ -0,0 +1,27 @@
+# Copyright 2016 Egor Tensin <Egor.Tensin@gmail.com>
+# This file is licensed under the terms of the MIT License.
+# See LICENSE.txt for details.
+
+from collections import OrderedDict
+from datetime import datetime
+
+from vk.user import Field
+
+class Record:
+ _FIELDS = (
+ Field.UID,
+ Field.FIRST_NAME,
+ Field.LAST_NAME,
+ Field.SCREEN_NAME,
+ Field.ONLINE,
+ Field.LAST_SEEN,
+ )
+
+ def __init__(self, user):
+ self._fields = OrderedDict()
+ for field in self._FIELDS:
+ self._fields[field] = user[field]
+ self._timestamp = datetime.utcnow().replace(microsecond=0)
+
+ def to_list(self):
+ return [self._timestamp.isoformat()] + list(self._fields.values())
diff --git a/vk/utils/tracking/db/writer/__init__.py b/vk/utils/tracking/db/writer/__init__.py
new file mode 100644
index 0000000..330d5a8
--- /dev/null
+++ b/vk/utils/tracking/db/writer/__init__.py
@@ -0,0 +1,5 @@
+# Copyright 2016 Egor Tensin <Egor.Tensin@gmail.com>
+# This file is licensed under the terms of the MIT License.
+# See LICENSE.txt for details.
+
+__all__ = 'csv',
diff --git a/vk/utils/tracking/db/writer/csv.py b/vk/utils/tracking/db/writer/csv.py
new file mode 100644
index 0000000..4594e09
--- /dev/null
+++ b/vk/utils/tracking/db/writer/csv.py
@@ -0,0 +1,40 @@
+# Copyright 2016 Egor Tensin <Egor.Tensin@gmail.com>
+# This file is licensed under the terms of the MIT License.
+# See LICENSE.txt for details.
+
+import csv
+from datetime import datetime
+
+from ..record import Record
+
+class Writer:
+ def __init__(self, path, mode='w'):
+ if path is None:
+ self._fd = None
+ else:
+ self._fd = open(path, mode)
+ self._writer = csv.writer(self._fd, lineterminator='\n')
+
+ def _is_valid(self):
+ return self._fd is not None
+
+ def __enter__(self):
+ if not self._is_valid():
+ return None
+ self._fd.__enter__()
+ return self
+
+ def __exit__(self, *args):
+ if self._is_valid():
+ self._fd.__exit__(*args)
+
+ def flush(self):
+ if self._is_valid():
+ self._fd.flush()
+
+ def write_record(self, user):
+ self._write_row(Record(user).to_list())
+ self.flush()
+
+ def _write_row(self, row):
+ self._writer.writerow(row)
diff --git a/vk/utils/tracking/logger.py b/vk/utils/tracking/logger.py
new file mode 100644
index 0000000..075935f
--- /dev/null
+++ b/vk/utils/tracking/logger.py
@@ -0,0 +1,60 @@
+# Copyright 2016 Egor Tensin <Egor.Tensin@gmail.com>
+# This file is licensed under the terms of the MIT License.
+# See LICENSE.txt for details.
+
+import logging
+import sys
+
+class Logger:
+ @staticmethod
+ def set_up(fd=sys.stdout):
+ logging.basicConfig(format='[%(asctime)s] %(message)s',
+ stream=fd,
+ level=logging.INFO,
+ datefmt='%Y-%m-%d %H:%M:%S')
+
+ @staticmethod
+ def on_initial_status(user):
+ if user.is_online():
+ logging.info(Logger._format_user_is_online(user))
+ else:
+ logging.info(Logger._format_user_is_offline(user))
+ logging.info(Logger._format_user_last_seen(user))
+
+ @staticmethod
+ def on_status_update(user):
+ if user.is_online():
+ logging.info(Logger._format_user_went_online(user))
+ else:
+ logging.info(Logger._format_user_went_offline(user))
+
+ @staticmethod
+ def on_exception(e):
+ logging.exception(e)
+
+ @staticmethod
+ def _format_user(user):
+ if user.has_last_name():
+ return '{} {}'.format(user.get_first_name(), user.get_last_name())
+ else:
+ return '{}'.format(user.get_first_name())
+
+ @staticmethod
+ def _format_user_is_online(user):
+ return '{} is ONLINE'.format(Logger._format_user(user))
+
+ @staticmethod
+ def _format_user_is_offline(user):
+ return '{} is OFFLINE'.format(Logger._format_user(user))
+
+ @staticmethod
+ def _format_user_last_seen(user):
+ return '{} was last seen at {}'.format(Logger._format_user(user), user.get_last_seen())
+
+ @staticmethod
+ def _format_user_went_online(user):
+ return '{} went ONLINE'.format(Logger._format_user(user))
+
+ @staticmethod
+ def _format_user_went_offline(user):
+ return '{} went OFFLINE'.format(Logger._format_user(user))
diff --git a/vk/utils/tracking/status_tracker.py b/vk/utils/tracking/status_tracker.py
new file mode 100644
index 0000000..dad14c0
--- /dev/null
+++ b/vk/utils/tracking/status_tracker.py
@@ -0,0 +1,94 @@
+# Copyright 2016 Egor Tensin <Egor.Tensin@gmail.com>
+# This file is licensed under the terms of the MIT License.
+# See LICENSE.txt for details.
+
+from collections import Callable
+import time
+
+import vk.error
+from vk.user import Field
+
+class StatusTracker:
+ DEFAULT_TIMEOUT = 5
+
+ def __init__(self, api, timeout=DEFAULT_TIMEOUT):
+ self._api = api
+ self._timeout = timeout
+ self._on_initial_status = []
+ self._on_status_update = []
+ self._on_connection_error = []
+
+ def _wait_after_connection_error(self):
+ time.sleep(self._timeout)
+
+ def add_initial_status_handler(self, fn):
+ self._assert_is_callback(fn)
+ self._on_initial_status.append(fn)
+
+ def add_status_update_handler(self, fn):
+ self._assert_is_callback(fn)
+ self._on_status_update.append(fn)
+
+ def add_connection_error_handler(self, fn):
+ self._assert_is_callback(fn)
+ self._on_connection_error.append(fn)
+
+ @staticmethod
+ def _assert_is_callback(fn):
+ if not isinstance(fn, Callable):
+ raise TypeError()
+
+ _USER_FIELDS = Field.SCREEN_NAME, Field.ONLINE, Field.LAST_SEEN
+
+ def _query_status(self, uids):
+ return {user.get_uid(): user for user in self._api.users_get(uids, self._USER_FIELDS)}
+
+ def _notify_status(self, user):
+ for fn in self._on_initial_status:
+ fn(user)
+
+ def _notify_status_update(self, user):
+ for fn in self._on_status_update:
+ fn(user)
+
+ def _notify_connection_error(self, e):
+ for fn in self._on_connection_error:
+ fn(e)
+
+ def _query_initial_status(self, uids):
+ while True:
+ try:
+ return self._query_status(uids)
+ except vk.error.ConnectionError as e:
+ self._notify_connection_error(e)
+ self._wait_after_connection_error()
+
+ def _query_status_updates(self, uids):
+ while True:
+ self._wait_after_connection_error()
+ try:
+ return self._query_status(uids)
+ except vk.error.ConnectionError as e:
+ self._notify_connection_error(e)
+
+ @staticmethod
+ def _filter_status_updates(old_users, new_users):
+ for uid, user in new_users.items():
+ if old_users[uid].is_online() != user.is_online():
+ old_users[uid] = user
+ yield user
+
+ def _do_loop(self, uids):
+ users = self._query_initial_status(uids)
+ for user in users.values():
+ self._notify_status(user)
+ while True:
+ updated_users = self._query_status_updates(uids)
+ for user in self._filter_status_updates(users, updated_users):
+ self._notify_status_update(user)
+
+ def loop(self, uids):
+ try:
+ self._do_loop(uids)
+ except KeyboardInterrupt:
+ pass