Document the new abstract session better

This commit is contained in:
Lonami Exo 2018-03-03 12:13:42 +01:00
parent 30f7a49263
commit 1e420f7f91
2 changed files with 116 additions and 11 deletions

View File

@ -37,13 +37,13 @@ To use a custom session storage, simply pass the custom session instance to
``TelegramClient`` instead of the session name. ``TelegramClient`` instead of the session name.
Currently, there are three implementations of the abstract ``Session`` class: Currently, there are three implementations of the abstract ``Session`` class:
* MemorySession. Stores session data in Python variables. * ``MemorySession``. Stores session data in Python variables.
* SQLiteSession, the default. Stores sessions in their own SQLite databases. * ``SQLiteSession``, (default). Stores sessions in their own SQLite databases.
* AlchemySession. Stores all sessions in a single database via SQLAlchemy. * ``AlchemySession``. Stores all sessions in a single database via SQLAlchemy.
Using AlchemySession Using AlchemySession
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
The AlchemySession implementation can store multiple Sessions in the same The ``AlchemySession`` implementation can store multiple Sessions in the same
database, but to do this, each session instance needs to have access to the database, but to do this, each session instance needs to have access to the
same models and database session. same models and database session.
@ -82,7 +82,9 @@ to ``False``:
... ...
container = AlchemySessionContainer(session=session, table_base=my_base, manage_tables=False) container = AlchemySessionContainer(
session=session, table_base=my_base, manage_tables=False
)
You always need to provide either ``engine`` or ``session`` to the container. You always need to provide either ``engine`` or ``session`` to the container.
If you set ``manage_tables=False`` and provide a ``session``, ``engine`` is not If you set ``manage_tables=False`` and provide a ``session``, ``engine`` is not
@ -101,9 +103,9 @@ where ``some session id`` is an unique identifier for the session.
Creating your own storage Creating your own storage
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
The easiest way to create your own implementation is to use MemorySession as The easiest way to create your own implementation is to use ``MemorySession``
the base and check out how ``SQLiteSession`` or ``AlchemySession`` work. You as the base and check out how ``SQLiteSession`` or ``AlchemySession`` work.
can find the relevant Python files under the ``sessions`` directory. You can find the relevant Python files under the ``sessions`` directory.
SQLite Sessions and Heroku SQLite Sessions and Heroku

View File

@ -6,6 +6,7 @@ import os
class Session(ABC): class Session(ABC):
def __init__(self): def __init__(self):
# Session IDs can be random on every connection
self.id = struct.unpack('q', os.urandom(8))[0] self.id = struct.unpack('q', os.urandom(8))[0]
self._sequence = 0 self._sequence = 0
@ -16,6 +17,9 @@ class Session(ABC):
self._flood_sleep_threshold = 60 self._flood_sleep_threshold = 60
def clone(self, to_instance=None): def clone(self, to_instance=None):
"""
Creates a clone of this session file.
"""
cloned = to_instance or self.__class__() cloned = to_instance or self.__class__()
cloned._report_errors = self.report_errors cloned._report_errors = self.report_errors
cloned._flood_sleep_threshold = self.flood_sleep_threshold cloned._flood_sleep_threshold = self.flood_sleep_threshold
@ -23,104 +27,195 @@ class Session(ABC):
@abstractmethod @abstractmethod
def set_dc(self, dc_id, server_address, port): def set_dc(self, dc_id, server_address, port):
"""
Sets the information of the data center address and port that
the library should connect to, as well as the data center ID,
which is currently unused.
"""
raise NotImplementedError raise NotImplementedError
@property @property
@abstractmethod @abstractmethod
def server_address(self): def server_address(self):
"""
Returns the server address where the library should connect to.
"""
raise NotImplementedError raise NotImplementedError
@property @property
@abstractmethod @abstractmethod
def port(self): def port(self):
"""
Returns the port to which the library should connect to.
"""
raise NotImplementedError raise NotImplementedError
@property @property
@abstractmethod @abstractmethod
def auth_key(self): def auth_key(self):
"""
Returns an ``AuthKey`` instance associated with the saved
data center, or ``None`` if a new one should be generated.
"""
raise NotImplementedError raise NotImplementedError
@auth_key.setter @auth_key.setter
@abstractmethod @abstractmethod
def auth_key(self, value): def auth_key(self, value):
"""
Sets the ``AuthKey`` to be used for the saved data center.
"""
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def close(self): def close(self):
raise NotImplementedError """
Called on client disconnection. Should be used to
free any used resources. Can be left empty if none.
"""
@abstractmethod @abstractmethod
def save(self): def save(self):
"""
Called whenever important properties change. It should
make persist the relevant session information to disk.
"""
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def delete(self): def delete(self):
"""
Called upon client.log_out(). Should delete the stored
information from disk since it's not valid anymore.
"""
raise NotImplementedError raise NotImplementedError
@classmethod @classmethod
@abstractmethod @abstractmethod
def list_sessions(cls): def list_sessions(cls):
"""
Lists available sessions. Not used by the library itself.
"""
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def process_entities(self, tlo): def process_entities(self, tlo):
"""
Processes the input ``TLObject`` or ``list`` and saves
whatever information is relevant (e.g., ID or access hash).
"""
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def get_input_entity(self, key): def get_input_entity(self, key):
"""
Turns the given key into an ``InputPeer`` (e.g. ``InputPeerUser``).
The library uses this method whenever an ``InputPeer`` is needed
to suit several purposes (e.g. user only provided its ID or wishes
to use a cached username to avoid extra RPC).
"""
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def cache_file(self, md5_digest, file_size, instance): def cache_file(self, md5_digest, file_size, instance):
"""
Caches the given file information persistently, so that it
doesn't need to be re-uploaded in case the file is used again.
The ``instance`` will be either an ``InputPhoto`` or ``InputDocument``,
both with an ``.id`` and ``.access_hash`` attributes.
"""
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def get_file(self, md5_digest, file_size, cls): def get_file(self, md5_digest, file_size, cls):
"""
Returns an instance of ``cls`` if the ``md5_digest`` and ``file_size``
match an existing saved record. The class will either be an
``InputPhoto`` or ``InputDocument``, both with two parameters
``id`` and ``access_hash`` in that order.
"""
raise NotImplementedError raise NotImplementedError
@property @property
def salt(self): def salt(self):
"""
Returns the current salt used when encrypting messages.
"""
return self._salt return self._salt
@salt.setter @salt.setter
def salt(self, value): def salt(self, value):
"""
Updates the salt (integer) used when encrypting messages.
"""
self._salt = value self._salt = value
@property @property
def report_errors(self): def report_errors(self):
"""
Whether RPC errors should be reported
to https://rpc.pwrtelegram.xyz or not.
"""
return self._report_errors return self._report_errors
@report_errors.setter @report_errors.setter
def report_errors(self, value): def report_errors(self, value):
"""
Sets the boolean value that indicates whether RPC errors
should be reported to https://rpc.pwrtelegram.xyz or not.
"""
self._report_errors = value self._report_errors = value
@property @property
def time_offset(self): def time_offset(self):
"""
Time offset (in seconds) to be used
in case the local time is incorrect.
"""
return self._time_offset return self._time_offset
@time_offset.setter @time_offset.setter
def time_offset(self, value): def time_offset(self, value):
"""
Updates the integer time offset in seconds.
"""
self._time_offset = value self._time_offset = value
@property @property
def flood_sleep_threshold(self): def flood_sleep_threshold(self):
"""
Threshold below which the library should automatically sleep
whenever a FloodWaitError occurs to prevent it from raising.
"""
return self._flood_sleep_threshold return self._flood_sleep_threshold
@flood_sleep_threshold.setter @flood_sleep_threshold.setter
def flood_sleep_threshold(self, value): def flood_sleep_threshold(self, value):
"""
Sets the new time threshold (integer, float or timedelta).
"""
self._flood_sleep_threshold = value self._flood_sleep_threshold = value
@property @property
def sequence(self): def sequence(self):
"""
Current sequence number needed to generate messages.
"""
return self._sequence return self._sequence
@sequence.setter @sequence.setter
def sequence(self, value): def sequence(self, value):
"""
Updates the sequence number (integer) value.
"""
self._sequence = value self._sequence = value
def get_new_msg_id(self): def get_new_msg_id(self):
"""Generates a new unique message ID based on the current """
time (in ms) since epoch""" Generates a new unique message ID based on the current
time (in ms) since epoch, applying a known time offset.
"""
now = time.time() + self._time_offset now = time.time() + self._time_offset
nanoseconds = int((now - int(now)) * 1e+9) nanoseconds = int((now - int(now)) * 1e+9)
new_msg_id = (int(now) << 32) | (nanoseconds << 2) new_msg_id = (int(now) << 32) | (nanoseconds << 2)
@ -133,12 +228,20 @@ class Session(ABC):
return new_msg_id return new_msg_id
def update_time_offset(self, correct_msg_id): def update_time_offset(self, correct_msg_id):
"""
Updates the time offset to the correct
one given a known valid message ID.
"""
now = int(time.time()) now = int(time.time())
correct = correct_msg_id >> 32 correct = correct_msg_id >> 32
self._time_offset = correct - now self._time_offset = correct - now
self._last_msg_id = 0 self._last_msg_id = 0
def generate_sequence(self, content_related): def generate_sequence(self, content_related):
"""
Generates the next sequence number depending on whether
it should be for a content-related query or not.
"""
if content_related: if content_related:
result = self._sequence * 2 + 1 result = self._sequence * 2 + 1
self._sequence += 1 self._sequence += 1