source: trunk/docs/contents/pg/db_wrapper.rst @ 770

Last change on this file since 770 was 770, checked in by cito, 4 years ago

Add methods for getting a table as a list or dict

Also added documentation and 100% test coverage.

The get_attnames() method now always returns a read-only ordered dictionary,
even under Python 2.6 or 3.0. So you can sure the columns will be returned
in the right order if you iterate over it, and that you don't accidentally
modify the dictionary (since it is cached).

File size: 27.2 KB
Line 
1The DB wrapper class
2====================
3
4.. py:currentmodule:: pg
5
6.. class:: DB
7
8The :class:`Connection` methods are wrapped in the class :class:`DB`
9which also adds convenient higher level methods for working with the
10database.  It also serves as a context manager for the connection.
11The preferred way to use this module is as follows::
12
13    import pg
14
15    with pg.DB(...) as db:  # for parameters, see below
16        for r in db.query(  # just for example
17                "SELECT foo, bar FROM foo_bar_table WHERE foo !~ bar"
18                ).dictresult():
19            print('%(foo)s %(bar)s' % r)
20
21This class can be subclassed as in this example::
22
23    import pg
24
25    class DB_ride(pg.DB):
26        """Ride database wrapper
27
28        This class encapsulates the database functions and the specific
29        methods for the ride database."""
30
31    def __init__(self):
32        """Open a database connection to the rides database"""
33        pg.DB.__init__(self, dbname='ride')
34        self.query("SET DATESTYLE TO 'ISO'")
35
36    [Add or override methods here]
37
38The following describes the methods and variables of this class.
39
40Initialization
41--------------
42The :class:`DB` class is initialized with the same arguments as the
43:func:`connect` function described above. It also initializes a few
44internal variables. The statement ``db = DB()`` will open the local
45database with the name of the user just like ``connect()`` does.
46
47You can also initialize the DB class with an existing :mod:`pg` or :mod:`pgdb`
48connection. Pass this connection as a single unnamed parameter, or as a
49single parameter named ``db``. This allows you to use all of the methods
50of the DB class with a DB-API 2 compliant connection. Note that the
51:meth:`Connection.close` and :meth:`Connection.reopen` methods are inoperative
52in this case.
53
54pkey -- return the primary key of a table
55-----------------------------------------
56
57.. method:: DB.pkey(table)
58
59    Return the primary key of a table
60
61    :param str table: name of table
62    :returns: Name of the field which is the primary key of the table
63    :rtype: str
64    :raises KeyError: the table does not have a primary key
65
66This method returns the primary key of a table.  Single primary keys are
67returned as strings unless you set the composite flag.  Composite primary
68keys are always represented as tuples.  Note that this raises a KeyError
69if the table does not have a primary key.
70
71get_databases -- get list of databases in the system
72----------------------------------------------------
73
74.. method:: DB.get_databases()
75
76    Get the list of databases in the system
77
78    :returns: all databases in the system
79    :rtype: list
80
81Although you can do this with a simple select, it is added here for
82convenience.
83
84get_relations -- get list of relations in connected database
85------------------------------------------------------------
86
87.. method:: DB.get_relations(kinds)
88
89    Get the list of relations in connected database
90
91    :param str kinds: a string or sequence of type letters
92    :returns: all relations of the given kinds in the database
93    :rtype: list
94
95The type letters are ``r`` = ordinary table, ``i`` = index, ``S`` = sequence,
96``v`` = view, ``c`` = composite type, ``s`` = special, ``t`` = TOAST table.
97If `kinds` is None or an empty string, all relations are returned (this is
98also the default). Although you can do this with a simple select, it is
99added here for convenience.
100
101get_tables -- get list of tables in connected database
102------------------------------------------------------
103
104.. method:: DB.get_tables()
105
106    Get the list of tables in connected database
107
108    :returns: all tables in connected database
109    :rtype: list
110
111This is a shortcut for ``get_relations('r')`` that has been added for
112convenience.
113
114get_attnames -- get the attribute names of a table
115--------------------------------------------------
116
117.. method:: DB.get_attnames(table)
118
119    Get the attribute names of a table
120
121    :param str table: name of table
122    :returns: an ordered dictionary mapping attribute names to type names
123
124Given the name of a table, digs out the set of attribute names.
125
126Returns a read-only dictionary of attribute names (the names are the keys,
127the values are the names of the attributes' types) with the column names
128in the proper order if you iterate over it.
129
130By default, only a limited number of simple types will be returned.
131You can get the regular types after enabling this by calling the
132:meth:`DB.use_regtypes` method.
133
134has_table_privilege -- check table privilege
135--------------------------------------------
136
137.. method:: DB.has_table_privilege(table, privilege)
138
139    Check whether current user has specified table privilege
140
141    :param str table: the name of the table
142    :param str privilege: privilege to be checked -- default is 'select'
143    :returns: whether current user has specified table privilege
144    :rtype: bool
145
146Returns True if the current user has the specified privilege for the table.
147
148.. versionadded:: 4.0
149
150get/set_parameter -- get or set  run-time parameters
151----------------------------------------------------
152
153.. method:: DB.get_parameter(parameter)
154
155    Get the value of run-time parameters
156
157    :param parameter: the run-time parameter(s) to get
158    :type param: str, tuple, list or dict
159    :returns: the current value(s) of the run-time parameter(s)
160    :rtype: str, list or dict
161    :raises TypeError: Invalid parameter type(s)
162    :raises pg.ProgrammingError: Invalid parameter name(s)
163
164If the parameter is a string, the return value will also be a string
165that is the current setting of the run-time parameter with that name.
166
167You can get several parameters at once by passing a list, set or dict.
168When passing a list of parameter names, the return value will be a
169corresponding list of parameter settings.  When passing a set of
170parameter names, a new dict will be returned, mapping these parameter
171names to their settings.  Finally, if you pass a dict as parameter,
172its values will be set to the current parameter settings corresponding
173to its keys.
174
175By passing the special name `'all'` as the parameter, you can get a dict
176of all existing configuration parameters.
177
178.. versionadded:: 4.2
179
180.. method:: DB.set_parameter(parameter, [value], [local])
181
182    Set the value of run-time parameters
183
184    :param parameter: the run-time parameter(s) to set
185    :type param: string, tuple, list or dict
186    :param value: the value to set
187    :type param: str or None
188    :raises TypeError: Invalid parameter type(s)
189    :raises ValueError: Invalid value argument(s)
190    :raises pg.ProgrammingError: Invalid parameter name(s) or values
191
192If the parameter and the value are strings, the run-time parameter
193will be set to that value.  If no value or *None* is passed as a value,
194then the run-time parameter will be restored to its default value.
195
196You can set several parameters at once by passing a list of parameter
197names, together with a single value that all parameters should be
198set to or with a corresponding list of values.  You can also pass
199the parameters as a set if you only provide a single value.
200Finally, you can pass a dict with parameter names as keys.  In this
201case, you should not pass a value, since the values for the parameters
202will be taken from the dict.
203
204By passing the special name `'all'` as the parameter, you can reset
205all existing settable run-time parameters to their default values.
206
207If you set *local* to `True`, then the command takes effect for only the
208current transaction.  After :meth:`DB.commit` or :meth:`DB.rollback`,
209the session-level setting takes effect again.  Setting *local* to `True`
210will appear to have no effect if it is executed outside a transaction,
211since the transaction will end immediately.
212
213.. versionadded:: 4.2
214
215begin/commit/rollback/savepoint/release -- transaction handling
216---------------------------------------------------------------
217
218.. method:: DB.begin([mode])
219
220    Begin a transaction
221
222    :param str mode: an optional transaction mode such as 'READ ONLY'
223
224    This initiates a transaction block, that is, all following queries
225    will be executed in a single transaction until :meth:`DB.commit`
226    or :meth:`DB.rollback` is called.
227
228.. versionadded:: 4.1
229
230.. method:: DB.start()
231
232    This is the same as the :meth:`DB.begin` method.
233
234.. method:: DB.commit()
235
236    Commit a transaction
237
238    This commits the current transaction. All changes made by the
239    transaction become visible to others and are guaranteed to be
240    durable if a crash occurs.
241
242.. method:: DB.end()
243
244    This is the same as the :meth:`DB.commit` method.
245
246.. versionadded:: 4.1
247
248.. method:: DB.rollback([name])
249
250    Roll back a transaction
251
252    :param str name: optionally, roll back to the specified savepoint
253
254    This rolls back the current transaction and causes all the updates
255    made by the transaction to be discarded.
256
257.. method:: DB.abort()
258
259    This is the same as the :meth:`DB.rollback` method.
260
261.. versionadded:: 4.2
262
263.. method:: DB.savepoint(name)
264
265    Define a new savepoint
266
267    :param str name: the name to give to the new savepoint
268
269    This establishes a new savepoint within the current transaction.
270
271.. versionadded:: 4.1
272
273.. method:: DB.release(name)
274
275    Destroy a savepoint
276
277    :param str name: the name of the savepoint to destroy
278
279    This destroys a savepoint previously defined in the current transaction.
280
281.. versionadded:: 4.1
282
283get -- get a row from a database table or view
284----------------------------------------------
285
286.. method:: DB.get(table, row, [keyname])
287
288    Get a row from a database table or view
289
290    :param str table: name of table or view
291    :param row: either a dictionary or the value to be looked up
292    :param str keyname: name of field to use as key (optional)
293    :returns: A dictionary - the keys are the attribute names,
294      the values are the row values.
295    :raises pg.ProgrammingError: table has no primary key or missing privilege
296    :raises KeyError: missing key value for the row
297
298This method is the basic mechanism to get a single row.  It assumes
299that the *keyname* specifies a unique row.  It must be the name of a
300single column or a tuple of column names.  If *keyname* is not specified,
301then the primary key for the table is used.
302
303If *row* is a dictionary, then the value for the key is taken from it.
304Otherwise, the row must be a single value or a tuple of values
305corresponding to the passed *keyname* or primary key.  The fetched row
306from the table will be returned as a new dictionary or used to replace
307the existing values when row was passed as aa dictionary.
308
309The OID is also put into the dictionary if the table has one, but
310in order to allow the caller to work with multiple tables, it is
311munged as ``oid(table)`` using the actual name of the table.
312
313insert -- insert a row into a database table
314--------------------------------------------
315
316.. method:: DB.insert(table, [row], [col=val, ...])
317
318    Insert a row into a database table
319
320    :param str table: name of table
321    :param dict row: optional dictionary of values
322    :param col: optional keyword arguments for updating the dictionary
323    :returns: the inserted values in the database
324    :rtype: dict
325    :raises pg.ProgrammingError: missing privilege or conflict
326
327This method inserts a row into a table.  If the optional dictionary is
328not supplied then the required values must be included as keyword/value
329pairs.  If a dictionary is supplied then any keywords provided will be
330added to or replace the entry in the dictionary.
331
332The dictionary is then reloaded with the values actually inserted in order
333to pick up values modified by rules, triggers, etc.
334
335update -- update a row in a database table
336------------------------------------------
337
338.. method:: DB.update(table, [row], [col=val, ...])
339
340    Update a row in a database table
341
342    :param str table: name of table
343    :param dict row: optional dictionary of values
344    :param col: optional keyword arguments for updating the dictionary
345    :returns: the new row in the database
346    :rtype: dict
347    :raises pg.ProgrammingError: table has no primary key or missing privilege
348    :raises KeyError: missing key value for the row
349
350Similar to insert but updates an existing row.  The update is based on
351the primary key of the table or the OID value as munged by :meth:`DB.get`
352or passed as keyword.
353
354The dictionary is then modified to reflect any changes caused by the
355update due to triggers, rules, default values, etc.
356
357Like insert, the dictionary is optional and updates will be performed
358on the fields in the keywords.  There must be an OID or primary key
359either in the dictionary where the OID must be munged, or in the keywords
360where it can be simply the string ``'oid'``.
361
362upsert -- insert a row with conflict resolution
363-----------------------------------------------
364
365.. method:: DB.upsert(table, [row], [col=val, ...])
366
367    Insert a row into a database table with conflict resolution
368
369    :param str table: name of table
370    :param dict row: optional dictionary of values
371    :param col: optional keyword arguments for specifying the update
372    :returns: the new row in the database
373    :rtype: dict
374    :raises pg.ProgrammingError: table has no primary key or missing privilege
375
376This method inserts a row into a table, but instead of raising a
377ProgrammingError exception in case a row with the same primary key already
378exists, an update will be executed instead.  This will be performed as a
379single atomic operation on the database, so race conditions can be avoided.
380
381Like the insert method, the first parameter is the name of the table and the
382second parameter can be used to pass the values to be inserted as a dictionary.
383
384Unlike the insert und update statement, keyword parameters are not used to
385modify the dictionary, but to specify which columns shall be updated in case
386of a conflict, and in which way:
387
388A value of `False` or `None` means the column shall not be updated,
389a value of `True` means the column shall be updated with the value that
390has been proposed for insertion, i.e. has been passed as value in the
391dictionary.  Columns that are not specified by keywords but appear as keys
392in the dictionary are also updated like in the case keywords had been passed
393with the value `True`.
394
395So if in the case of a conflict you want to update every column that has been
396passed in the dictionary `d` , you would call ``upsert(table, d)``.  If you
397don't want to do anything in case of a conflict, i.e. leave the existing row
398as it is, call ``upsert(table, d, **dict.fromkeys(d))``.
399
400If you need more fine-grained control of what gets updated, you can also pass
401strings in the keyword parameters.  These strings will be used as SQL
402expressions for the update columns.  In these expressions you can refer
403to the value that already exists in the table by writing the table prefix
404``included.`` before the column name, and you can refer to the value that
405has been proposed for insertion by writing ``excluded.`` as table prefix.
406
407The dictionary is modified in any case to reflect the values in the database
408after the operation has completed.
409
410.. note::
411
412    The method uses the PostgreSQL "upsert" feature which is only available
413    since PostgreSQL 9.5. With older PostgreSQL versions, you will get a
414    ProgrammingError if you use this method.
415
416.. versionadded:: 5.0
417
418query -- execute a SQL command string
419-------------------------------------
420
421.. method:: DB.query(command, [arg1, [arg2, ...]])
422
423    Execute a SQL command string
424
425    :param str command: SQL command
426    :param arg*: optional positional arguments
427    :returns: result values
428    :rtype: :class:`Query`, None
429    :raises TypeError: bad argument type, or too many arguments
430    :raises TypeError: invalid connection
431    :raises ValueError: empty SQL query or lost connection
432    :raises pg.ProgrammingError: error in query
433    :raises pg.InternalError: error during query processing
434
435Similar to the :class:`Connection` function with the same name, except that
436positional arguments can be passed either as a single list or tuple, or as
437individual positional arguments.
438
439Example::
440
441    name = input("Name? ")
442    phone = input("Phone? ")
443    rows = db.query("update employees set phone=$2 where name=$1",
444        (name, phone)).getresult()[0][0]
445    # or
446    rows = db.query("update employees set phone=$2 where name=$1",
447         name, phone).getresult()[0][0]
448
449clear -- clear row values in memory
450-----------------------------------
451
452.. method:: DB.clear(table, [row])
453
454    Clear row values in memory
455
456    :param str table: name of table
457    :param dict row: optional dictionary of values
458    :returns: an empty row
459    :rtype: dict
460
461This method clears all the attributes to values determined by the types.
462Numeric types are set to 0, Booleans are set to ``'f'``, and everything
463else is set to the empty string.  If the row argument is present, it is
464used as the row dictionary and any entries matching attribute names are
465cleared with everything else left unchanged.
466
467If the dictionary is not supplied a new one is created.
468
469delete -- delete a row from a database table
470--------------------------------------------
471
472.. method:: DB.delete(table, [row], [col=val, ...])
473
474    Delete a row from a database table
475
476    :param str table: name of table
477    :param dict d: optional dictionary of values
478    :param col: optional keyword arguments for updating the dictionary
479    :rtype: None
480    :raises pg.ProgrammingError: table has no primary key,
481        row is still referenced or missing privilege
482    :raises KeyError: missing key value for the row
483
484This method deletes the row from a table.  It deletes based on the
485primary key of the table or the OID value as munged by :meth:`DB.get`
486or passed as keyword.
487
488The return value is the number of deleted rows (i.e. 0 if the row did not
489exist and 1 if the row was deleted).
490
491Note that if the row cannot be deleted because e.g. it is still referenced
492by another table, this method will raise a ProgrammingError.
493
494truncate -- quickly empty database tables
495-----------------------------------------
496
497.. method:: DB.truncate(table, [restart], [cascade], [only])
498
499    Empty a table or set of tables
500
501    :param table: the name of the table(s)
502    :type table: str, list or set
503    :param bool restart: whether table sequences should be restarted
504    :param bool cascade: whether referenced tables should also be truncated
505    :param only: whether only parent tables should be truncated
506    :type only: bool or list
507
508This method quickly removes all rows from the given table or set
509of tables.  It has the same effect as an unqualified DELETE on each
510table, but since it does not actually scan the tables it is faster.
511Furthermore, it reclaims disk space immediately, rather than requiring
512a subsequent VACUUM operation. This is most useful on large tables.
513
514If *restart* is set to `True`, sequences owned by columns of the truncated
515table(s) are automatically restarted.  If *cascade* is set to `True`, it
516also truncates all tables that have foreign-key references to any of
517the named tables.  If the parameter *only* is not set to `True`, all the
518descendant tables (if any) will also be truncated. Optionally, a ``*``
519can be specified after the table name to explicitly indicate that
520descendant tables are included.  If the parameter *table* is a list,
521the parameter *only* can also be a list of corresponding boolean values.
522
523.. versionadded:: 4.2
524
525get_as_list/dict -- read a table as a list or dictionary
526--------------------------------------------------------
527
528.. method:: DB.get_as_list(table, [what], [where], [order], [limit], [offset], [scalar])
529
530    Get a table as a list
531
532    :param str table: the name of the table (the FROM clause)
533    :param what: column(s) to be returned (the SELECT clause)
534    :type what: str, list, tuple or None
535    :param where: conditions(s) to be fulfilled (the WHERE clause)
536    :type where: str, list, tuple or None
537    :param order: column(s) to sort by (the ORDER BY clause)
538    :type order: str, list, tuple, False or None
539    :param int limit: maximum number of rows returned (the LIMIT clause)
540    :param int offset: number of rows to be skipped (the OFFSET clause)
541    :param bool scalar: whether only the first column shall be returned
542    :returns: the content of the table as a list
543    :rtype: list
544    :raises TypeError: the table name has not been specified
545
546This gets a convenient representation of the table as a list of named tuples
547in Python.  You only need to pass the name of the table (or any other SQL
548expression returning rows).  Note that by default this will return the full
549content of the table which can be huge and overflow your memory.  However, you
550can control the amount of data returned using the other optional parameters.
551
552The parameter *what* can restrict the query to only return a subset of the
553table columns.  The parameter *where* can restrict the query to only return a
554subset of the table rows.  The specified SQL expressions all need to be
555fulfilled for a row to get into the result.  The parameter *order* specifies
556the ordering of the rows.  If no ordering is specified, the result will be
557ordered by the primary key(s) or all columns if no primary key exists.
558You can set *order* to *False* if you don't care about the ordering.
559The parameters *limit* and *offset* specify the maximum number of rows
560returned and a number of rows skipped over.
561
562If you set the *scalar* option to *True*, then instead of the named tuples
563you will get the first items of these tuples.  This is useful if the result
564has only one column anyway.
565
566.. method:: DB.get_as_dict(table, [keyname], [what], [where], [order], [limit], [offset], [scalar])
567
568    Get a table as a dictionary
569
570    :param str table: the name of the table (the FROM clause)
571    :param keyname: column(s) to be used as key(s) of the dictionary
572    :type keyname: str, list, tuple or None
573    :param what: column(s) to be returned (the SELECT clause)
574    :type what: str, list, tuple or None
575    :param where: conditions(s) to be fulfilled (the WHERE clause)
576    :type where: str, list, tuple or None
577    :param order: column(s) to sort by (the ORDER BY clause)
578    :type order: str, list, tuple, False or None
579    :param int limit: maximum number of rows returned (the LIMIT clause)
580    :param int offset: number of rows to be skipped (the OFFSET clause)
581    :param bool scalar: whether only the first column shall be returned
582    :returns: the content of the table as a list
583    :rtype: dict or OrderedDict
584    :raises TypeError: the table name has not been specified
585    :raises KeyError: keyname(s) are invalid or not part of the result
586    :raises pg.ProgrammingError: no keyname(s) and table has no primary key
587
588This method is similar to :meth:`DB.get_as_list`, but returns the table as
589a Python dict instead of a Python list, which can be even more convenient.
590The primary key column(s) of the table will be used as the keys of the
591dictionary, while the other column(s) will be the corresponding values.
592The keys will be named tuples if the table has a composite primary key.
593The rows will be also named tuples unless the *scalar* option has been set
594to *True*.  With the optional parameter *keyname* you can specify a different
595set of columns to be used as the keys of the dictionary.
596
597If the Python version supports it, the dictionary will be an *OrderedDict*
598using the order specified with the *order* parameter or the key column(s)
599if not specified.  You can set *order* to *False* if you don't care about the
600ordering.  In this case the returned dictionary will be an ordinary one.
601
602escape_literal -- escape a literal string for use within SQL
603------------------------------------------------------------
604
605.. method:: DB.escape_literal(string)
606
607    Escape a string for use within SQL as a literal constant
608
609    :param str string: the string that is to be escaped
610    :returns: the escaped string
611    :rtype: str
612
613This method escapes a string for use within an SQL command. This is useful
614when inserting data values as literal constants in SQL commands. Certain
615characters (such as quotes and backslashes) must be escaped to prevent them
616from being interpreted specially by the SQL parser.
617
618.. versionadded:: 4.1
619
620escape_identifier -- escape an identifier string for use within SQL
621-------------------------------------------------------------------
622
623.. method:: DB.escape_identifier(string)
624
625    Escape a string for use within SQL as an identifier
626
627    :param str string: the string that is to be escaped
628    :returns: the escaped string
629    :rtype: str
630
631This method escapes a string for use as an SQL identifier, such as a table,
632column, or function name. This is useful when a user-supplied identifier
633might contain special characters that would otherwise not be interpreted
634as part of the identifier by the SQL parser, or when the identifier might
635contain upper case characters whose case should be preserved.
636
637.. versionadded:: 4.1
638
639escape_string -- escape a string for use within SQL
640---------------------------------------------------
641
642.. method:: DB.escape_string(string)
643
644    Escape a string for use within SQL
645
646    :param str string: the string that is to be escaped
647    :returns: the escaped string
648    :rtype: str
649
650Similar to the module function with the same name, but the
651behavior of this method is adjusted depending on the connection properties
652(such as character encoding).
653
654escape_bytea -- escape binary data for use within SQL
655-----------------------------------------------------
656
657.. method:: DB.escape_bytea(datastring)
658
659    Escape binary data for use within SQL as type ``bytea``
660
661    :param str datastring: string containing the binary data that is to be escaped
662    :returns: the escaped string
663    :rtype: str
664
665Similar to the module function with the same name, but the
666behavior of this method is adjusted depending on the connection properties
667(in particular, whether standard-conforming strings are enabled).
668
669unescape_bytea -- unescape data that has been retrieved as text
670---------------------------------------------------------------
671
672.. method:: DB.unescape_bytea(string)
673
674    Unescape ``bytea`` data that has been retrieved as text
675
676    :param datastring: the ``bytea`` data string that has been retrieved as text
677    :returns: byte string containing the binary data
678    :rtype: bytes
679
680See the module function with the same name.
681
682use_regtypes -- determine use of regular type names
683---------------------------------------------------
684
685.. method:: DB.use_regtypes([regtypes])
686
687    Determine whether regular type names shall be used
688
689    :param bool regtypes: if passed, set whether regular type names shall be used
690    :returns: whether regular type names are used
691
692The :meth:`DB.get_attnames` method can return either simplified "classic"
693type names (the default) or more specific "regular" type names. Which kind
694of type names is used can be changed by calling :meth:`DB.get_regtypes`.
695If you pass a boolean, it sets whether regular type names shall be used.
696The method can also be used to check through its return value whether
697currently regular type names are used.
698
699.. versionadded:: 4.1
700
701notification_handler -- create a notification handler
702-----------------------------------------------------
703
704.. class:: DB.notification_handler(event, callback, [arg_dict], [timeout], [stop_event])
705
706    Create a notification handler instance
707
708    :param str event: the name of an event to listen for
709    :param callback: a callback function
710    :param dict arg_dict: an optional dictionary for passing arguments
711    :param timeout: the time-out when waiting for notifications
712    :type timeout: int, float or None
713    :param str stop_event: an optional different name to be used as stop event
714
715This method creates a :class:`pg.NotificationHandler` object using the
716:class:`DB` connection as explained under :doc:`notification`.
717
718.. versionadded:: 4.1.1
Note: See TracBrowser for help on using the repository browser.