Package Gnumed :: Package pycommon :: Module gmBusinessDBObject |
[frames] | no frames]

# Source Code for Module Gnumed.pycommon.gmBusinessDBObject

      1  """GNUmed database object business class.
      2
      3  Overview
      4  --------
      5  This class wraps a source relation (table, view) which
      6  represents an entity that makes immediate business sense
      7  such as a vaccination or a medical document. In many if
      8  not most cases this source relation is a denormalizing
      9  view. The data in that view will in most cases, however,
     10  originate from several normalized tables. One instance
     11  of this class represents one row of said source relation.
     12
     13  Note, however, that this class does not *always* simply
     14  wrap a single table or view. It can also encompass several
     15  relations (views, tables, sequences etc) that taken together
     16  form an object meaningful to *business* logic.
     17
     18  Initialization
     19  --------------
     20  There are two ways to initialize an instance with values.
     21  One way is to pass a "primary key equivalent" object into
     22  __init__(). Refetch_payload() will then pull the data from
     23  the backend. Another way would be to fetch the data outside
     24  the instance and pass it in via the <row> argument. In that
     25  case the instance will not initially connect to the databse
     26  which may offer a great boost to performance.
     27
     28  Values API
     29  ----------
     30  Field values are cached for later access. They can be accessed
     31  by a dictionary API, eg:
     32
     33          old_value = object['field']
     34          object['field'] = new_value
     35
     36  The field names correspond to the respective column names
     37  in the "main" source relation. Accessing non-existant field
     38  names will raise an error, so does trying to set fields not
     39  listed in self.__class__._updatable_fields. To actually
     40  store updated values in the database one must explicitly
     41  call save_payload().
     42
     43  The class will in many cases be enhanced by accessors to
     44  related data that is not directly part of the business
     45  object itself but are closely related, such as codes
     46  linked to a clinical narrative entry (eg a diagnosis). Such
     47  accessors in most cases start with get_*. Related setters
     48  start with set_*. The values can be accessed via the
     49  object['field'] syntax, too, but they will be cached
     50  independantly.
     51
     52  Concurrency handling
     53  --------------------
     54  GNUmed connections always run transactions in isolation level
     55  "serializable". This prevents transactions happening at the
     56  *very same time* to overwrite each other's data. All but one
     57  of them will abort with a concurrency error (eg if a
     58  transaction runs a select-for-update later than another one
     59  it will hang until the first transaction ends. Then it will
     60  succeed or fail depending on what the first transaction
     61  did). This is standard transactional behaviour.
     62
     63  However, another transaction may have updated our row
     64  between the time we first fetched the data and the time we
     65  start the update transaction. This is noticed by getting the
     66  XMIN system column for the row when initially fetching the
     67  data and using that value as a where condition value when
     68  updating the row later. If the row had been updated (xmin
     69  changed) or deleted (primary key disappeared) in the
     70  meantime the update will touch zero rows (as no row with
     71  both PK and XMIN matching is found) even if the query itself
     72  syntactically succeeds.
     73
     74  When detecting a change in a row due to XMIN being different
     75  one needs to be careful how to represent that to the user.
     76  The row may simply have changed but it also might have been
     77  deleted and a completely new and unrelated row which happens
     78  to have the same primary key might have been created ! This
     79  row might relate to a totally different context (eg. patient,
     80  episode, encounter).
     81
     82  One can offer all the data to the user:
     83
     84  self.original_payload
     85  - contains the data at the last successful refetch
     86
     87  self.modified_payload
     88  - contains the modified payload just before the last
     89    failure of save_payload() - IOW what is currently
     90    in the database
     91
     92  self._payload
     93  - contains the currently active payload which may or
     94    may not contain changes
     95
     96  For discussion on this see the thread starting at:
     97
     98          http://archives.postgresql.org/pgsql-general/2004-10/msg01352.php
     99
    100  and here
    101
    102          http://groups.google.com/group/pgsql.general/browse_thread/thread/e3566ba76173d0bf/6cf3c243a86d9233
    103          (google for "XMIN semantic at peril")
    104
    105  Problem cases with XMIN:
    106
    107  1) not unlikely
    108  - a very old row is read with XMIN
    109  - vacuum comes along and sets XMIN to FrozenTransactionId
    110    - now XMIN changed but the row actually didn't !
    111  - an update with "... where xmin = old_xmin ..." fails
    112    although there is no need to fail
    113
    114  2) quite unlikely
    115  - a row is read with XMIN
    116  - a long time passes
    117  - the original XMIN gets frozen to FrozenTransactionId
    118  - another writer comes along and changes the row
    119  - incidentally the exact same old row gets the old XMIN *again*
    120    - now XMIN is (again) the same but the data changed !
    121  - a later update fails to detect the concurrent change !!
    122
    123  TODO:
    124  The solution is to use our own column for optimistic locking
    125  which gets updated by an AFTER UPDATE trigger.
    126  """
    127  #============================================================
    128  __version__ = "$Revision: 1.60 $"
    129  __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
    130  __license__ = "GPL"
    131
    132  import sys, copy, types, inspect, logging, datetime
    133
    134
    135  if __name__ == '__main__':
    136          sys.path.insert(0, '../../')
    137  from Gnumed.pycommon import gmExceptions, gmPG2
    138
    139
    140  _log = logging.getLogger('gm.db')
    141  _log.info(__version__)
    142  #============================================================
    143 -class cBusinessDBObject(object):
    144          """Represents business objects in the database.
    145
    146          Rules:
    147          - instances ARE ASSUMED TO EXIST in the database
    148          - PK construction (aPK_obj): DOES verify its existence on instantiation
    149                                       (fetching data fails)
    150          - Row construction (row): allowed by using a dict of pairs
    151                                         field name: field value (PERFORMANCE improvement)
    152          - does NOT verify FK target existence
    153          - does NOT create new entries in the database
    154          - does NOT lazy-fetch fields on access
    155
    156          Class scope SQL commands and variables:
    157
    158          <_cmd_fetch_payload>
    159                  - must return exactly one row
    160                  - where clause argument values are expected
    161                    in self.pk_obj (taken from __init__(aPK_obj))
    162                  - must return xmin of all rows that _cmds_store_payload
    163                    will be updating, so views must support the xmin columns
    164                    of their underlying tables
    165
    166          <_cmds_store_payload>
    167                  - one or multiple "update ... set ... where xmin_* = ..." statements
    168                    which actually update the database from the data in self._payload,
    169                  - the last query must refetch at least the XMIN values needed to detect
    170                    concurrent updates, their field names had better be the same as
    171                    in _cmd_fetch_payload,
    172                  - when subclasses tend to live a while after save_payload() was
    173                    called and they support computed fields (say, _(some_column)
    174                    you need to return *all* columns (see cEncounter)
    175
    176          <_updatable_fields>
    177                  - a list of fields available for update via object['field']
    178
    179
    180          A template for new child classes:
    181
    182  *********** start of template ***********
    183
    184  #------------------------------------------------------------
    185  from Gnumed.pycommon import gmBusinessDBObject
    186  from Gnumed.pycommon import gmPG2
    187
    188  #============================================================
    189  # short description
    190  #------------------------------------------------------------
    191  # use plural form, search-replace get_XXX
    192  _SQL_get_XXX = u\"""
    193          SELECT *, (xmin AS xmin_XXX)
    194          FROM XXX.v_XXX
    195          WHERE %s
    196  \"""
    197
    198  class cXxxXxx(gmBusinessDBObject.cBusinessDBObject):
    199          \"""Represents ...\"""
    200
    201          _cmd_fetch_payload = _SQL_get_XXX % u"pk_XXX = %s"
    202          _cmds_store_payload = [
    203                  u\"""
    204                          UPDATE xxx.xxx SET                                                              -- typically the underlying table name
    205                                  xxx = %(xxx)s,                                                          -- typically "table_col = %(view_col)s"
    206                                  xxx = gm.nullify_empty_string(%(xxx)s)
    207                          WHERE
    208                                  pk = %(pk_XXX)s
    209                                          AND
    210                                  xmin = %(xmin_XXX)s
    211                          RETURNING
    212                                  pk as pk_XXX,
    213                                  xmin as xmin_XXX
    214                  \"""
    215          ]
    216          # view columns that can be updated:
    217          _updatable_fields = [
    218                  u'xxx',
    219                  u'xxx'
    220          ]
    221  #------------------------------------------------------------
    222  def get_XXX(order_by=None):
    223          if order_by is None:
    224                  order_by = u'true'
    225          else:
    226                  order_by = u'true ORDER BY %s' % order_by
    227
    228          cmd = _SQL_get_XXX % order_by
    229          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
    230          return [ cXxxXxx(row = {'data': r, 'idx': idx, 'pk_field': 'xxx'}) for r in rows ]
    231  #------------------------------------------------------------
    232  def create_xxx(xxx=None, xxx=None):
    233
    234          args = {
    235                  u'xxx': xxx,
    236                  u'xxx': xxx
    237          }
    238          cmd = u\"""
    239                  INSERT INTO xxx.xxx (
    240                          xxx,
    241                          xxx,
    242                          xxx
    243                  ) VALUES (
    244                          %(xxx)s,
    245                          %(xxx)s,
    246                          gm.nullify_empty_string(%(xxx)s)
    247                  )
    248                  RETURNING pk
    249          \"""
    250          rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
    251
    252          return cXxxXxx(aPK_obj = rows[0]['pk'])
    253  #------------------------------------------------------------
    254  def delete_xxx(xxx=None):
    255          args = {'pk': xxx}
    256          cmd = u"DELETE FROM xxx.xxx WHERE pk = %(pk)s"
    257          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    258          return True
    259  #------------------------------------------------------------
    260
    261  *********** end of template ***********
    262
    263          """
    264          #--------------------------------------------------------
    265 -        def __init__(self, aPK_obj=None, row=None):
    266                  """Init business object.
    267
    268                  Call from child classes:
    269
    270                          super(cChildClass, self).__init__(aPK_obj = aPK_obj, row = row)
    271                  """
    272                  # initialize those "too early" because checking descendants might
    273                  # fail which will then call __str__ in stack trace logging if --debug
    274                  # was given which in turn needs those instance variables
    275                  self.pk_obj = '<uninitialized>'
    276                  self._idx = {}
    277                  self._payload = []              # the cache for backend object values (mainly table fields)
    278                  self._ext_cache = {}    # the cache for extended method's results
    279                  self._is_modified = False
    280
    281                  # check descendants
    282                  self.__class__._cmd_fetch_payload
    283                  self.__class__._cmds_store_payload
    284                  self.__class__._updatable_fields
    285
    286                  if aPK_obj is not None:
    287                          self.__init_from_pk(aPK_obj=aPK_obj)
    288                  else:
    289                          self._init_from_row_data(row=row)
    290
    291                  self._is_modified = False
    292          #--------------------------------------------------------
    293 -        def __init_from_pk(self, aPK_obj=None):
    294                  """Creates a new clinical item instance by its PK.
    295
    296                  aPK_obj can be:
    297                          - a simple value
    298                            * the primary key WHERE condition must be
    299                                  a simple column
    300                          - a dictionary of values
    301                            * the primary key where condition must be a
    302                                  subselect consuming the dict and producing
    303                                  the single-value primary key
    304                  """
    305                  self.pk_obj = aPK_obj
    306                  result = self.refetch_payload()
    307                  if result is True:
    308                          self.original_payload = {}
    309                          for field in self._idx.keys():
    310                                  self.original_payload[field] = self._payload[self._idx[field]]
    311                          return True
    312
    313                  if result is False:
    314                          raise gmExceptions.ConstructorError, "[%s:%s]: error loading instance" % (self.__class__.__name__, self.pk_obj)
    315          #--------------------------------------------------------
    316 -        def _init_from_row_data(self, row=None):
    317                  """Creates a new clinical item instance given its fields.
    318
    319                  row must be a dict with the fields:
    320                          - pk_field: the name of the primary key field
    321                          - idx: a dict mapping field names to position
    322                          - data: the field values in a list (as returned by
    323                            cursor.fetchone() in the DB-API)
    324
    325                  row = {'data': row, 'idx': idx, 'pk_field': 'the PK column name'}
    326
    327                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    328                  objects = [ cChildClass(row = {'data': r, 'idx': idx, 'pk_field': 'the PK column name'}) for r in rows ]
    329                  """
    330                  try:
    331                          self._idx = row['idx']
    332                          self._payload = row['data']
    333                          self.pk_obj = self._payload[self._idx[row['pk_field']]]
    334                  except:
    335                          _log.exception('faulty <row> argument structure: %s' % row)
    336                          raise gmExceptions.ConstructorError, "[%s:??]: error loading instance from row data" % self.__class__.__name__
    337
    338                  if len(self._idx.keys()) != len(self._payload):
    339                          _log.critical('field index vs. payload length mismatch: %s field names vs. %s fields' % (len(self._idx.keys()), len(self._payload)))
    340                          _log.critical('faulty <row> argument structure: %s' % row)
    341                          raise gmExceptions.ConstructorError, "[%s:??]: error loading instance from row data" % self.__class__.__name__
    342
    343                  self.original_payload = {}
    344                  for field in self._idx.keys():
    345                          self.original_payload[field] = self._payload[self._idx[field]]
    346          #--------------------------------------------------------
    347 -        def __del__(self):
    348                  if self.__dict__.has_key('_is_modified'):
    349                          if self._is_modified:
    350                                  _log.critical('[%s:%s]: loosing payload changes' % (self.__class__.__name__, self.pk_obj))
    351                                  _log.debug('original: %s' % self.original_payload)
    352                                  _log.debug('modified: %s' % self._payload)
    353          #--------------------------------------------------------
    354 -        def __str__(self):
    355                  tmp = []
    356                  try:
    357                          for attr in self._idx.keys():
    358                                  if self._payload[self._idx[attr]] is None:
    359                                          tmp.append(u'%s: NULL' % attr)
    360                                  else:
    361                                          tmp.append('%s: >>%s<<' % (attr, self._payload[self._idx[attr]]))
    362                          return '[%s:%s]: %s' % (self.__class__.__name__, self.pk_obj, str(tmp))
    363                  except:
    364                          return 'nascent [%s @ %s], cannot show payload and primary key' %(self.__class__.__name__, id(self))
    365          #--------------------------------------------------------
    366 -        def __getitem__(self, attribute):
    367                  # use try: except: as it is faster and we want this as fast as possible
    368
    369                  # 1) backend payload cache
    370                  try:
    371                          return self._payload[self._idx[attribute]]
    372                  except KeyError:
    373                          pass
    374
    375                  # 2) extension method results ...
    376                  getter = getattr(self, 'get_%s' % attribute, None)
    377                  if not callable(getter):
    378                          _log.warning('[%s]: no attribute [%s]' % (self.__class__.__name__, attribute))
    379                          _log.warning('[%s]: valid attributes: %s' % (self.__class__.__name__, str(self._idx.keys())))
    380                          _log.warning('[%s]: no getter method [get_%s]' % (self.__class__.__name__, attribute))
    381                          methods = filter(lambda x: x[0].startswith('get_'), inspect.getmembers(self, inspect.ismethod))
    382                          _log.warning('[%s]: valid getter methods: %s' % (self.__class__.__name__, str(methods)))
    383                          raise KeyError('[%s]: cannot read from key [%s]' % (self.__class__.__name__, attribute))
    384
    385                  self._ext_cache[attribute] = getter()
    386                  return self._ext_cache[attribute]
    387          #--------------------------------------------------------
    388 -        def __setitem__(self, attribute, value):
    389
    390                  # 1) backend payload cache
    391                  if attribute in self.__class__._updatable_fields:
    392                          try:
    393                                  if self._payload[self._idx[attribute]] != value:
    394                                          self._payload[self._idx[attribute]] = value
    395                                          self._is_modified = True
    396                                  return
    397                          except KeyError:
    398                                  _log.warning('[%s]: cannot set attribute <%s> despite marked settable' % (self.__class__.__name__, attribute))
    399                                  _log.warning('[%s]: supposedly settable attributes: %s' % (self.__class__.__name__, str(self.__class__._updatable_fields)))
    400                                  raise KeyError('[%s]: cannot write to key [%s]' % (self.__class__.__name__, attribute))
    401
    402                  # 2) setters providing extensions
    403                  if hasattr(self, 'set_%s' % attribute):
    404                          setter = getattr(self, "set_%s" % attribute)
    405                          if not callable(setter):
    406                                  raise AttributeError('[%s] setter [set_%s] not callable' % (self.__class__.__name__, attribute))
    407                          try:
    408                                  del self._ext_cache[attribute]
    409                          except KeyError:
    410                                  pass
    411                          if type(value) is types.TupleType:
    412                                  if setter(*value):
    413                                          self._is_modified = True
    414                                          return
    415                                  raise AttributeError('[%s]: setter [%s] failed for [%s]' % (self.__class__.__name__, setter, value))
    416                          if setter(value):
    417                                  self._is_modified = True
    418                                  return
    419
    420                  # 3) don't know what to do with <attribute>
    421                  _log.error('[%s]: cannot find attribute <%s> or setter method [set_%s]' % (self.__class__.__name__, attribute, attribute))
    422                  _log.warning('[%s]: settable attributes: %s' % (self.__class__.__name__, str(self.__class__._updatable_fields)))
    423                  methods = filter(lambda x: x[0].startswith('set_'), inspect.getmembers(self, inspect.ismethod))
    424                  _log.warning('[%s]: valid setter methods: %s' % (self.__class__.__name__, str(methods)))
    425                  raise AttributeError('[%s]: cannot set [%s]' % (self.__class__.__name__, attribute))
    426          #--------------------------------------------------------
    427          # external API
    428          #--------------------------------------------------------
    429 -        def same_payload(self, another_object=None):
    430                  raise NotImplementedError('comparison between [%s] and [%s] not implemented' % (self, another_object))
    431          #--------------------------------------------------------
    432 -        def is_modified(self):
    433                  return self._is_modified
    434          #--------------------------------------------------------
    435 -        def get_fields(self):
    436                  try:
    437                          return self._idx.keys()
    438                  except AttributeError:
    439                          return 'nascent [%s @ %s], cannot return keys' %(self.__class__.__name__, id(self))
    440          #--------------------------------------------------------
    441 -        def get_updatable_fields(self):
    442                  return self.__class__._updatable_fields
    443          #--------------------------------------------------------
    444 -        def fields_as_dict(self, date_format='%c'):
    445                  data = {}
    446                  for field in self._idx.keys():
    447                          val = self._payload[self._idx[field]]
    448                          if val is None:
    449                                  data[field] = u''
    450                                  continue
    451                          if isinstance(val, datetime.datetime):
    452                                  try:
    453                                          data[field] = val.strftime(date_format).decode('utf8', 'replace')
    454                                  except ValueError:
    455                                          data[field] = val.isoformat()
    456                                  continue
    457                          try:
    458                                  data[field] = unicode(val, encoding = 'utf8', errors = 'replace')
    459                          except TypeError:
    460                                  try:
    461                                          data[field] = unicode(val)
    462                                  except (UnicodeDecodeError, TypeError):
    463                                          val = '%s' % str(val)
    464                                          data[field] = val.decode('utf8', 'replace')
    465                  return data
    466          #--------------------------------------------------------
    467 -        def get_patient(self):
    468                  _log.error('[%s:%s]: forgot to override get_patient()' % (self.__class__.__name__, self.pk_obj))
    469                  return None
    470          #--------------------------------------------------------
    471 -        def refetch_payload(self, ignore_changes=False):
    472                  """Fetch field values from backend.
    473                  """
    474                  if self._is_modified:
    475                          if ignore_changes:
    476                                  _log.critical('[%s:%s]: loosing payload changes' % (self.__class__.__name__, self.pk_obj))
    477                                  _log.debug('original: %s' % self.original_payload)
    478                                  _log.debug('modified: %s' % self._payload)
    479                          else:
    480                                  _log.critical('[%s:%s]: cannot reload, payload changed' % (self.__class__.__name__, self.pk_obj))
    481                                  return False
    482
    483                  if type(self.pk_obj) == types.DictType:
    484                          arg = self.pk_obj
    485                  else:
    486                          arg = [self.pk_obj]
    487                  rows, self._idx = gmPG2.run_ro_queries (
    488                          queries = [{'cmd': self.__class__._cmd_fetch_payload, 'args': arg}],
    489                          get_col_idx = True
    490                  )
    491                  if len(rows) == 0:
    492                          _log.error('[%s:%s]: no such instance' % (self.__class__.__name__, self.pk_obj))
    493                          return False
    494                  self._payload = rows[0]
    495                  return True
    496          #--------------------------------------------------------
    497 -        def __noop(self):
    498                  pass
    499          #--------------------------------------------------------
    500 -        def save(self, conn=None):
    501                  return self.save_payload(conn = conn)
    502          #--------------------------------------------------------
    503 -        def save_payload(self, conn=None):
    504                  """Store updated values (if any) in database.
    505
    506                  Optionally accepts a pre-existing connection
    507                  - returns a tuple (<True|False>, <data>)
    508                  - True: success
    509                  - False: an error occurred
    510                          * data is (error, message)
    511                          * for error meanings see gmPG2.run_rw_queries()
    512                  """
    513                  if not self._is_modified:
    514                          return (True, None)
    515
    516                  args = {}
    517                  for field in self._idx.keys():
    518                          args[field] = self._payload[self._idx[field]]
    519                  self.modified_payload = args
    520
    521                  close_conn = self.__noop
    522                  if conn is None:
    523                          conn = gmPG2.get_connection(readonly=False)
    524                          close_conn = conn.close
    525
    526                  # query succeeded but failed to find the row to lock
    527                  # because another transaction committed an UPDATE or
    528                  # DELETE *before* we attempted to lock it ...
    529                  # FIXME: this can fail if savepoints are used since subtransactions change the xmin/xmax ...
    530
    531                  queries = []
    532                  for query in self.__class__._cmds_store_payload:
    533                          queries.append({'cmd': query, 'args': args})
    534                  rows, idx = gmPG2.run_rw_queries (
    535                          link_obj = conn,
    536                          queries = queries,
    537                          return_data = True,
    538                          get_col_idx = True
    539                  )
    540
    541                  # this can happen if:
    542                  # - someone else updated the row so XMIN does not match anymore
    543                  # - the PK went away (rows was deleted from under us)
    544                  # - another WHERE condition of the UPDATE did not produce any rows to update
    545                  if len(rows) == 0:
    546                          return (False, (u'cannot update row', _('[%s:%s]: row not updated (nothing returned), row in use ?') % (self.__class__.__name__, self.pk_obj)))
    547
    548                  # update cached XMIN values (should be in first-and-only result row of last query)
    549                  row = rows[0]
    550                  for key in idx:
    551                          try:
    552                                  self._payload[self._idx[key]] = row[idx[key]]
    553                          except KeyError:
    554                                  conn.rollback()
    555                                  close_conn()
    556                                  _log.error('[%s:%s]: cannot update instance, XMIN refetch key mismatch on [%s]' % (self.__class__.__name__, self.pk_obj, key))
    557                                  _log.error('payload keys: %s' % str(self._idx))
    558                                  _log.error('XMIN refetch keys: %s' % str(idx))
    559                                  _log.error(args)
    560                                  raise
    561
    562                  conn.commit()
    563                  close_conn()
    564
    565                  self._is_modified = False
    566                  # update to new "original" payload
    567                  self.original_payload = {}
    568                  for field in self._idx.keys():
    569                          self.original_payload[field] = self._payload[self._idx[field]]
    570
    571                  return (True, None)
    572
    573  #============================================================
    574 -def jsonclasshintify(obj):
    575          # this should eventually be somewhere else
    576          """ turn the data into a list of dicts, adding "class hints".
    577                  all objects get turned into dictionaries which the other end
    578                  will interpret as "object", via the __jsonclass__ hint,
    579                  as specified by the JSONRPC protocol standard.
    580          """
    581          if isinstance(obj, list):
    582                  return map(jsonclasshintify, obj)
    583          elif isinstance(obj, gmPG2.dbapi.tz.FixedOffsetTimezone):
    584                  # this will get decoded as "from jsonobjproxy import {clsname}"
    585                  # at the remote (client) end.
    586                  res = {'__jsonclass__': ["jsonobjproxy.FixedOffsetTimezone"]}
    587                  res['name'] = obj._name
    588                  res['offset'] = jsonclasshintify(obj._offset)
    589                  return res
    590          elif isinstance(obj, datetime.timedelta):
    591                  # this will get decoded as "from jsonobjproxy import {clsname}"
    592                  # at the remote (client) end.
    593                  res = {'__jsonclass__': ["jsonobjproxy.TimeDelta"]}
    594                  res['days'] = obj.days
    595                  res['seconds'] = obj.seconds
    596                  res['microseconds'] = obj.microseconds
    597                  return res
    598          elif isinstance(obj, datetime.time):
    599                  # this will get decoded as "from jsonobjproxy import {clsname}"
    600                  # at the remote (client) end.
    601                  res = {'__jsonclass__': ["jsonobjproxy.Time"]}
    602                  res['hour'] = obj.hour
    603                  res['minute'] = obj.minute
    604                  res['second'] = obj.second
    605                  res['microsecond'] = obj.microsecond
    606                  res['tzinfo'] = jsonclasshintify(obj.tzinfo)
    607                  return res
    608          elif isinstance(obj, datetime.datetime):
    609                  # this will get decoded as "from jsonobjproxy import {clsname}"
    610                  # at the remote (client) end.
    611                  res = {'__jsonclass__': ["jsonobjproxy.DateTime"]}
    612                  res['year'] = obj.year
    613                  res['month'] = obj.month
    614                  res['day'] = obj.day
    615                  res['hour'] = obj.hour
    616                  res['minute'] = obj.minute
    617                  res['second'] = obj.second
    618                  res['microsecond'] = obj.microsecond
    619                  res['tzinfo'] = jsonclasshintify(obj.tzinfo)
    620                  return res
    621          elif isinstance(obj, cBusinessDBObject):
    622                  # this will get decoded as "from jsonobjproxy import {clsname}"
    623                  # at the remote (client) end.
    624                  res = {'__jsonclass__': ["jsonobjproxy.%s" % obj.__class__.__name__]}
    625                  for k in obj.get_fields():
    626                          t = jsonclasshintify(obj[k])
    627                          res[k] = t
    628                  print "props", res, dir(obj)
    629                  for attribute in dir(obj):
    630                          if not attribute.startswith("get_"):
    631                                  continue
    632                          k = attribute[4:]
    633                          if res.has_key(k):
    634                                  continue
    635                          getter = getattr(obj, attribute, None)
    636                          if callable(getter):
    637                                  res[k] = jsonclasshintify(getter())
    638                  return res
    639          return obj
    640
    641  #============================================================
    642  if __name__ == '__main__':
    643
    644          if len(sys.argv) < 2:
    645                  sys.exit()
    646
    647          if sys.argv[1] != u'test':
    648                  sys.exit()
    649
    650          #--------------------------------------------------------
    651 -        class cTestObj(cBusinessDBObject):
    652                  _cmd_fetch_payload = None
    653                  _cmds_store_payload = None
    654                  _updatable_fields = []
    655                  #----------------------------------------------------
    656 -                def get_something(self):
    657                          pass
    658                  #----------------------------------------------------
    659 -                def set_something(self):
    660                          pass
    661          #--------------------------------------------------------
    662          from Gnumed.pycommon import gmI18N
    663          gmI18N.activate_locale()
    664          gmI18N.install_domain()
    665
    666          data = {
    667                  'pk_field': 'bogus_pk',
    668                  'idx': {'bogus_pk': 0, 'bogus_field': 1, 'bogus_date': 2},
    669                  'data': [-1, 'bogus_data', datetime.datetime.now()]
    670          }
    671          obj = cTestObj(row=data)
    672          #print obj['wrong_field']
    673          #print jsonclasshintify(obj)
    674          #obj['wrong_field'] = 1
    675          print obj.fields_as_dict()
    676
    677  #============================================================
    678