aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorEgor Tensin <Egor.Tensin@gmail.com>2016-06-16 02:40:51 +0300
committerEgor Tensin <Egor.Tensin@gmail.com>2016-06-16 02:40:51 +0300
commit8bd8f1f2d09db33697fc61d5caaa3b37b856916d (patch)
treebdb11964cdaf5c985663638f9dbedd5167e3ee4b
parenttrack_status.py: add a database of statuses (diff)
downloadvk-scripts-8bd8f1f2d09db33697fc61d5caaa3b37b856916d.tar.gz
vk-scripts-8bd8f1f2d09db33697fc61d5caaa3b37b856916d.zip
track_status.py: move stuff to the package
Diffstat (limited to '')
-rw-r--r--track_status.py201
-rw-r--r--vk/user.py102
-rw-r--r--vk/utils/__init__.py0
-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
10 files changed, 320 insertions, 217 deletions
diff --git a/track_status.py b/track_status.py
index 81693bd..9f22ea3 100644
--- a/track_status.py
+++ b/track_status.py
@@ -2,201 +2,12 @@
# This file is licensed under the terms of the MIT License.
# See LICENSE.txt for details.
-from collections import Callable
-import csv
-from datetime import datetime
-import logging
-import sys
-import time
-
import vk.api
-import vk.error
-from vk.user import Field
-
-class CSVWriter:
- def __init__(self, path):
- if path is None:
- self._fd = None
- else:
- self._fd = open(path, 'w')
- 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_status(self, user):
- self._write_row(self._status_to_row(user))
- self.flush()
-
- def _write_row(self, row):
- self._writer.writerow(row)
-
- @staticmethod
- def _status_to_row(user):
- return [
- datetime.utcnow().replace(microsecond=0).isoformat(),
- user.get_uid(),
- user.get_first_name(),
- user.get_last_name(),
- user.get_screen_name(),
- user.is_online(),
- ]
-
-class Logger:
- @staticmethod
- def set_up(stream=sys.stdout):
- logging.basicConfig(format='[%(asctime)s] %(message)s',
- stream=stream,
- 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))
-
-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, StatusTracker.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
+from vk.utils.tracking import Logger, StatusTracker
+from vk.utils.tracking.db.writer import *
if __name__ == '__main__':
- import argparse
+ import argparse, sys
def natural_number(s):
x = int(s)
@@ -229,11 +40,11 @@ if __name__ == '__main__':
tracker.add_status_update_handler(Logger.on_status_update)
tracker.add_connection_error_handler(Logger.on_exception)
- with CSVWriter(args.output) as csv_writer:
+ with csv.Writer(args.output) as csv_writer:
if csv_writer is not None:
- tracker.add_initial_status_handler(lambda user: csv_writer.write_status(user))
- tracker.add_status_update_handler(lambda user: csv_writer.write_status(user))
+ tracker.add_initial_status_handler(lambda user: csv_writer.write_record(user))
+ tracker.add_status_update_handler(lambda user: csv_writer.write_record(user))
try:
tracker.loop(args.uids)
diff --git a/vk/user.py b/vk/user.py
index 397d902..a895960 100644
--- a/vk/user.py
+++ b/vk/user.py
@@ -4,6 +4,7 @@
from datetime import datetime
from enum import Enum
+from numbers import Real, Integral
class Field(Enum):
UID = 'uid'
@@ -20,51 +21,108 @@ class User:
def __init__(self, impl):
self._impl = impl
+ def __str__(self):
+ return str(self._impl)
+
+ def __eq__(self, other):
+ return self.get_uid() == other.get_uid()
+
+ def __hash__(self):
+ return hash(self.get_uid())
+
def __iter__(self):
return iter(self._impl)
+ def __contains__(self, field):
+ if field is Field.LAST_SEEN:
+ return self._has_last_seen()
+ return self._normalize_field(field) in self._impl
+
def __getitem__(self, field):
- if isinstance(field, Field):
- field = field.value
- return self._impl[field]
+ if field is Field.LAST_SEEN:
+ return self._get_last_seen()
+ return self._impl[self._normalize_field(field)]
- def __contains__(self, field):
+ def __setitem__(self, field, value):
+ if field is Field.LAST_SEEN:
+ self._set_last_seen(value)
+ else:
+ self._impl[self._normalize_field(field)] = value
+
+ @staticmethod
+ def _normalize_field(field):
if isinstance(field, Field):
- field = field.value
- return field in self._impl
+ return field.value
+ return field
def get_uid(self):
- return self._impl[Field.UID.value]
+ return self[Field.UID]
def get_first_name(self):
- return self._impl[Field.FIRST_NAME.value]
+ return self[Field.FIRST_NAME]
- def get_last_name(self):
- return self._impl[Field.LAST_NAME.value]
+ def set_first_name(self, name):
+ self[Field.FIRST_NAME] = name
def has_last_name(self):
- return Field.LAST_NAME.value in self._impl and self.get_last_name()
+ return Field.LAST_NAME in self and self.get_last_name()
+
+ def get_last_name(self):
+ return self[Field.LAST_NAME]
+
+ def set_last_name(self, name):
+ self[Field.LAST_NAME] = name
def has_screen_name(self):
- return Field.SCREEN_NAME.value in self._impl
+ return Field.SCREEN_NAME in self
def get_screen_name(self):
if self.has_screen_name():
- return self._impl[Field.SCREEN_NAME.value]
+ return self[Field.SCREEN_NAME]
else:
return 'id' + str(self.get_uid())
+ def set_screen_name(self, name):
+ self[Field.SCREEN_NAME] = name
+
+ def has_online(self):
+ return Field.ONLINE in self
+
def is_online(self):
- return self._impl[Field.ONLINE.value]
+ return bool(self[Field.ONLINE])
- def get_last_seen(self):
- return datetime.fromtimestamp(self._impl[Field.LAST_SEEN.value]['time'])
+ def set_online(self, value=True):
+ self[Field.ONLINE] = value
- def __str__(self):
- return repr(self._impl)
+ @staticmethod
+ def _last_seen_from_timestamp(t):
+ return datetime.fromtimestamp(t)
- def __hash__(self):
- return hash(self.get_uid())
+ @staticmethod
+ def _last_seen_to_timestamp(t):
+ if isinstance(t, datetime):
+ return t.timestamp()
+ elif isinstance(t, Real) or isinstance(t, Integral):
+ return t
+ else:
+ raise TypeError('"last seen" time must be either a `datetime` or a POSIX timestamp')
- def __eq__(self, other):
- return self.get_uid() == other.get_uid()
+ def _has_last_seen(self):
+ return Field.LAST_SEEN.value in self._impl and 'time' in self._impl[Field.LAST_SEEN.value]
+
+ def has_last_seen(self):
+ return self._has_last_seen()
+
+ def _get_last_seen(self):
+ return self._last_seen_from_timestamp(self._impl[Field.LAST_SEEN.value]['time'])
+
+ def get_last_seen(self):
+ return self._get_last_seen()
+
+ def _set_last_seen(self, t):
+ if Field.LAST_SEEN.value not in self._impl:
+ self._impl[Field.LAST_SEEN.value] = {}
+ self._impl[Field.LAST_SEEN.value]['time'] = self._last_seen_to_timestamp(t)
+
+ def set_last_seen(self, t):
+ self._set_last_seen(t)
diff --git a/vk/utils/__init__.py b/vk/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/vk/utils/__init__.py
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