# -*- coding: utf-8 -*-
#------------------------------------------------------------------------------
# file: $Id: context.py 53 2012-08-28 06:11:48Z griff1n $
# lib: pysyncml.context
# auth: griffin <griffin@uberdev.org>
# date: 2012/06/23
# copy: (C) CopyLoose 2012 UberDev <hardcore@uberdev.org>, No Rights Reserved.
#------------------------------------------------------------------------------
'''
The ``pysyncml.context`` package provides the entry point into most
pysyncml operations via the :class:`pysyncml.Context
<pysyncml.context.Context>` class.
'''
import sqlalchemy.ext.declarative
from sqlalchemy.orm.exc import NoResultFound
from . import model, codec, router, protocol, synchronizer
#------------------------------------------------------------------------------
[docs]class Context(object):
'''
The pysyncml Context object creates an environment for an Adapter to
be created and evaluated in. The primary object is to provide access
to a storage location so that SyncML state information can be stored
across multiple synchronization messages, sessions and
peers. However, it can also be used to specify operational
parameters, such as whether or not to support specific codecs and
which to use as the default.
'''
#----------------------------------------------------------------------------
def __init__(self,
engine=None, storage=None, prefix='pysyncml', owner=None,
autoCommit=None,
router=None, protocol=None, synchronizer=None, codec=None,
):
'''
The Context constructor accepts the following parameters, of which
all are optional:
:param owner:
an integer owner ID. Necessary primarily when the adapter
storage is shared between multiple users/adapter agents
(i.e. in server contexts). If it is not shared, `owner` can be
left as ``None`` (the default).
:param storage:
the sqlalchemy storage specification where all the SyncML-
related data should be stored.
NOTE: can be overridden by parameter `engine`.
NOTE: the storage driver **MUST** support cascading deletes;
this is done automatically for connections created directly by
pysyncml for mySQL and sqlite, but it is up to the calling
program to ensure this for other databases or if the database
engine is passed in via parameter `engine`. Specifically, when
pysyncml creates the sqlalchemy engine (i.e. by calling
``sqlalchemy.create_engine(storage)``), then InnoDB is requested
for mySQL tables and ``PRAGMA foreign_keys=ON`` is issued for
sqlite databases. pysyncml provides a helper function to ensure
that sqlite databases have cascading deletes enabled::
import sqlalchemy, pysyncml
db = sqlalchemy.create_engine(...)
pysyncml.enableSqliteCascadingDeletes(db)
:param engine:
the sqlalchemy storage engine where all the SyncML-related
data should be stored.
NOTE: overrides parameter `storage`.
NOTE: see notes under parameter `storage` for details on
cascading delete support.
TODO: it would be great to add a check to ensure that provided
storage engines have cascading deletes enabled.
:param prefix:
sets a database table name prefix. This is primarily useful when
using the `engine` parameter, as multiple pysyncml contexts can
then be defined within the same database namespace. Defaults to
``pysyncml``.
:param autoCommit:
whether or not to execute a storage engine "commit" when syncing
is complete. The default behavior is dependent on if `engine` is
provided: if not ``None``, then `autoCommit` defaults to
``False``, otherwise, defaults to ``True``.
:param router:
overrides the default router with an object that must implement
the interface specified by :class:`pysyncml.router.Router`.
:param protocol:
sets the semantic objective to/from protocol evaluation and
resolution object, which must implement the
:class:`pysyncml.protocol.Protocol` interface.
:param synchronizer:
this is the engine for handling sync requests and dispatching
them to the various agents. If specified, the object must
implement the :class:`pysyncml.synchronizer.Synchronizer`
interface.
:param codec:
specify the codec used to encode the SyncML commands - typically
either ``\'xml\'`` (the default) or ``\'wbxml\'``. It can also
be an object that implements the :class:`pysyncml.codec.Codec`
interface.
'''
self.autoCommit = autoCommit if autoCommit is not None else engine is None
self._model = model.createModel(
engine = engine,
storage = storage,
prefix = prefix,
owner_id = owner,
context = self,
)
self.router = router
self.protocol = protocol
self.synchronizer = synchronizer
self.codec = codec
for attr in dir(self._model):
if attr in ('DatabaseObject', 'RawDatabaseObject', 'Version', 'Adapter'):
continue
value = getattr(self._model, attr)
if issubclass(value.__class__, sqlalchemy.ext.declarative.DeclarativeMeta) \
and value != self._model.DatabaseObject:
setattr(self, attr, value)
#----------------------------------------------------------------------------
# TODO: add a method to delete all entries with a specific owner...
#----------------------------------------------------------------------------
#----------------------------------------------------------------------------
[docs] def Adapter(self, **kw):
'''
.. TODO:: move this documentation into model/adapter.py?...
The Adapter constructor supports the following parameters:
:param devID:
sets the local adapter\'s device identifier. For servers, this
should be the externally accessible URL that launches the SyncML
transaction, and for clients this should be a unique ID, such as
the IMEI number (for mobile phones). If not specified, it will
be defaulted to the `devID` of the `devinfo` object. If it
cannot be loaded from the database or from the `devinfo`, then
it must be provided before any synchronization can begin.
:param name:
sets the local adapter\'s device name - usually a human-friendly
description of this SyncML\'s function.
:param devinfo:
sets the local adapter :class:`pysyncml.devinfo.DeviceInfo`. If
not specified, it will be auto-loaded from the database. If it
cannot be loaded from the database, then it must be provided
before any synchronization can begin.
:param peer:
TODO: document...
:param maxGuidSize:
TODO: document...
:param maxMsgSize:
TODO: document...
:param maxObjSize:
TODO: document...
:param conflictPolicy:
sets the default conflict handling policy for this adapter,
and can be overriden on a per-store basis (applies only when
operating as the server role).
'''
try:
ret = self._model.Adapter.q(isLocal=True).one()
for k, v in kw.items():
setattr(ret, k, v)
except NoResultFound:
ret = self._model.Adapter(**kw)
ret.isLocal = True
self._model.session.add(ret)
if ret.devID is not None:
self._model.session.flush()
ret.context = self
# todo: is this really the best place to do this?...
ret.router = self.router or router.Router(ret)
ret.protocol = self.protocol or protocol.Protocol(ret)
ret.synchronizer = self.synchronizer or synchronizer.Synchronizer(ret)
ret.codec = self.codec or 'xml'
if isinstance(ret.codec, basestring):
ret.codec = codec.Codec.factory(ret.codec)
if ret.devID is not None:
peers = ret.getKnownPeers()
if len(peers) == 1 and peers[0].url is not None:
ret._peer = peers[0]
return ret
#----------------------------------------------------------------------------
[docs] def RemoteAdapter(self, **kw):
'''
.. TODO:: move this documentation into model/adapter.py?...
The RemoteAdapter constructor supports the following parameters:
:param url:
specifies the URL that this remote SyncML server can be reached
at. The URL must be a fully-qualified URL.
:param auth:
set what kind of authentication scheme to use, which generally is
one of the following values:
**None**:
indicates no authentication is required.
**pysyncml.NAMESPACE_AUTH_BASIC**:
specifies to use "Basic-Auth" authentication scheme.
**pysyncml.NAMESPACE_AUTH_MD5**:
specifies to use MD5 "Digest-Auth" authentication scheme.
NOTE: this may not be implemented yet...
:param username:
if the `auth` is not ``None``, then the username to authenticate
as must be provided via this parameter.
:param password:
if the `auth` is not ``None``, then the password to authenticate
with must be provided via this parameter.
'''
# TODO: is this really the right way?...
ret = self._model.Adapter(isLocal=False, **kw)
self._model.session.add(ret)
if ret.devID is not None:
self._model.session.flush()
return ret
#----------------------------------------------------------------------------
@staticmethod
def getAuthInfo(request, authorizer):
xtree = codec.Codec.autoDecode(request.headers['content-type'], request.body)
return protocol.Protocol.getAuthInfo(xtree, None, authorizer)
#----------------------------------------------------------------------------
@staticmethod
def getTargetID(request):
xtree = codec.Codec.autoDecode(request.headers['content-type'], request.body)
return protocol.Protocol.getTargetID(xtree)
#----------------------------------------------------------------------------
def save(self):
# TODO: is this just here for the test classes?... might this be better
# marked as an internal method?...
# todo: is the "flush" really necessary?...
if self.autoCommit:
self._model.session.flush()
self._model.session.commit()
#------------------------------------------------------------------------------
# end of $Id: context.py 53 2012-08-28 06:11:48Z griff1n $
#------------------------------------------------------------------------------