Module control.mongo
Expand source code Browse git
import os
from bson import ObjectId, BSON, decode_all
from bson.json_util import dumps as dumpjs
from pymongo import MongoClient
from .generic import AttrDict, deepAttrDict, isonow
from .files import dirMake, dirExists
MDEL = "markedDeleted"
MDELDT = "dateDeleted"
MDELBY = "deletedBy"
MRESDT = "dateUndeleted"
MRESBY = "undeletedBy"
class Mongo:
@staticmethod
def cast(value):
"""Try to cast the value as an ObjectId.
Paramaters
----------
value:string
The value to cast, normally a string representation of a BSON ObjectId.
Returns
-------
ObjectId | void
The corresponding BSON ObjectId if the input is a valid representation of
such an id, otherwise `None`.
"""
if value is None:
return None
if isinstance(value, ObjectId):
return value
try:
oValue = ObjectId(value)
except Exception:
oValue = None
return oValue
@staticmethod
def isId(value):
"""Test whether a value is an ObjectId
Parameters
----------
value: any
The value to test
Returns
-------
boolean
Whether the value is an objectId
"""
return isinstance(value, ObjectId)
def __init__(self, Settings, Messages):
"""CRUD interface to content in the MongoDb database.
This class has methods to connect to a MongoDb database,
to query its data, to create, update and delete data.
It is instantiated by a singleton object.
!!! note "string versus ObjectId"
Some functions execute MongoDb statements, based on parameters
whose values are MongoDb identifiers.
These should be objects in the class `bson.objectid.ObjectId`.
However, in many cases these ids enter the app as strings.
In this module, such strings will be cast to proper ObjectIds,
provided they are recognizable as values in a field whose name is
`_id` or ends with `Id`.
!!! note "Deletion policy"
When a user deletes records, they will not be deleted, but instead they
get `markedDeleted: true`. A periodic job (control.sweeper)
will physically remove deleted records after 31 days.
Also, deleted records will be listed on the admin interface. Admins can
restore them within 30 days.
See also
[deletion strategy](https://github.com/CLARIAH/pure3dx/blob/main/docs/architecture.md#deletion-and-sweeping).
Most methods that access the database know this policy. So if a
list of records satisfying some criteria is asked, no records that
are marked for deletion are returned.
In other words: such records are treated as if they do not exist.
Some of these methods have an optional parameter `deleted=False`.
When True, it restricts its operation on records that are marked
as deleted.
Some methods do not implement this policy:
* `executeMongo()`
* `hardDelete...()`
* `mkBackup()`
* `restoreBackup()`
Marking is done by adding `markedDeleted: true` to a record.
Unmarking is done by removing the key `markedDeleted` from a record.
So the test whether a record `r` is marked for deletion is:
`r.get("markedDeleted", False)`
And the mongo query expression to find unmarked records only is:
`{markedDeleted: {$exists: false}}`
Parameters
----------
Settings: AttrDict
App-wide configuration data obtained from
`control.config.Config.Settings`.
Messages: object
Singleton instance of `control.messages.Messages`.
"""
self.Settings = Settings
runMode = Settings.runMode
self.Messages = Messages
Messages.debugAdd(self)
self.client = None
self.db = None
self.database = f"{Settings.database}_{runMode}"
def connect(self):
"""Make connection with MongoDb if there is no connection yet.
The connection details come from `control.config.Config.Settings`.
After a successful connection attempt, the connection handle
is stored in the `client` and `db` members of the Mongo object.
When a connection handle exists, this method does nothing.
"""
Messages = self.Messages
Settings = self.Settings
client = self.client
db = self.db
database = self.database
if db is None:
try:
client = MongoClient(
Settings.mongoHost,
Settings.mongoPort,
username=Settings.mongoUser,
password=Settings.mongoPassword,
)
db = client[database]
except Exception as e:
Messages.error(
msg="Could not connect to the database",
logmsg=f"Mongo connection: `{e}`",
)
self.client = client
self.db = db
def disconnect(self):
"""Disconnect from the MongoDB."""
client = self.client
if client:
client.close()
self.client = None
self.db = None
def tables(self):
"""List the existent tables in the database.
Returns
-------
list
The names of the tables.
"""
self.connect()
db = self.db
return list(db.list_collection_names())
def clearTable(self, table, delete=False):
"""Make sure that a table exists and that it is empty.
Parameters
----------
table: string
The name of the table.
If no such table exists, it will be created.
delete: boolean, optional False
If True, and the table existed before, it will be deleted.
If False, the table will be cleared, i.e. all its records
get deleted, but the table remains.
"""
Messages = self.Messages
self.connect()
db = self.db
if delete:
if db[table] is not None:
try:
db.drop_collection(table)
Messages.plain(
msg=f"dropped table `{table}`",
logmsg=f"dropped table `{table}`",
)
except Exception as e:
Messages.error(
msg="Database action",
logmsg=f"Cannot delete table: `{table}`: {e}",
)
else:
if db[table] is None:
try:
db.create_collection(table)
except Exception as e:
Messages.error(
msg="Database action",
logmsg=f"Cannot create table: `{table}`: {e}",
)
else:
(good, count) = self.deleteRecordsHard(table, {}, "system")
if good:
plural = "" if count == 1 else "s"
Messages.plain(
msg=f"cleared table `{table} of {count} record{plural}`",
logmsg=f"cleared table `{table} of {count} record{plural}`",
)
def get(self, table, record, deleted=False):
"""Get the record and recordId if only one of them is specified.
If the record is specified by id, the id maybe an ObjectId or a string,
which will then be cast to an ObjectId.
Parameters
----------
table: string
The table in which the record can be found
record: string | ObjectID | AttrDict | void
Either the id of the record, or the record itself.
deleted: boolean, optional False
Search only in the records that are marked for deletion
Returns
-------
tuple
* ObjectId: the id of the record
* AttrDict: the record itself
If `record` is None, both members of the tuple are None
"""
if record is None:
return (None, None)
if type(record) is str:
record = self.getRecord(table, dict(_id=self.cast(record)), deleted=deleted)
elif self.isId(record):
record = self.getRecord(table, dict(_id=record), deleted=deleted)
recordId = record._id
return (recordId, record)
def getRecord(self, table, criteria, deleted=False, warn=True):
"""Get a single record from a table.
Parameters
----------
table: string
The name of the table from which we want to retrieve a single record.
criteria: dict
A set of criteria to narrow down the search.
Usually they will be such that there will be just one record
that satisfies them.
But if there are more, a single one is chosen,
by the mechanics of the built-in MongoDb command `findOne`.
deleted: boolean, optional False
Search only in the records that are marked for deletion
warn: boolean, optional True
If True, warn if there is no record satisfying the criteria.
Returns
-------
AttrDict
The single record found,
or an empty AttrDict if no record
satisfies the criteria.
"""
Messages = self.Messages
criteria[MDEL] = {"$exists": True} if deleted else None
(good, result) = self.executeMongo(table, "find_one", criteria, {}, warn=False)
if not good or result is None:
if warn:
Messages.warning(
msg=f"Could not find that {table}",
logmsg=f"No record in {table} with {criteria}",
)
result = {}
return deepAttrDict(result)
def getList(self, table, criteria, deleted=False, sort=None, asDict=False):
"""Get a list of records from a table.
Parameters
----------
table: string
The name of the table from which we want to retrieve records.
criteria: dict
A set of criteria to narrow down the search.
deleted: boolean, optional False
Search only in the records that are marked for deletion
sort: string | function, optional None
Sort key. If `None`, the results will not be sorted.
If a string, it is the name of a field by which the results
will be sorted in ascending order.
If a function, the function should take a record as input and return a
value. The records will be sorted by this value.
asDict: boolean or string, optional False
If False, returns a list of records as result. If True or a string, returns
the same records, but now as dict, keyed by the `_id` field if
asDict is True, else keyed by the field in dictated by asDict.
Returns
-------
list of AttrDict
The list of records found, empty if no records are found.
Each record is cast to an AttrDict.
"""
criteria[MDEL] = {"$exists": True} if deleted else None
(good, result) = self.executeMongo(table, "find", criteria, {})
if not good:
return []
unsorted = [deepAttrDict(record) for record in result]
if sort is None:
result = unsorted
else:
sortFunc = (lambda r: r[sort] or "") if type(sort) is str else sort
result = sorted(unsorted, key=sortFunc)
return (
{r[asDict]: r for r in result}
if type(asDict) is str
else {r._id: r for r in result} if asDict else result
)
def hardDeleteRecord(self, table, criteria, by):
"""Deletes a single record from a table.
Parameters
----------
table: string
The name of the table from which we want to delete a single record.
criteria: dict
A set of criteria to narrow down the selection.
Usually they will be such that there will be just one record
that satisfies them.
But if there are more, a single one is chosen,
by the mechanics of the built-in MongoDb command `updateOne`.
by: string
The name of the user who issued the command
Returns
-------
boolean
Whether the delete was successful
"""
(good, result) = self.executeMongo(table, "delete_one", criteria)
return result.deleted_count > 0 if good else False
def deleteRecord(self, table, criteria, by):
"""Deletes a single record from a table.
If the record has already been deleted, nothing is done.
Parameters
----------
table: string
The name of the table from which we want to delete a single record.
criteria: dict
A set of criteria to narrow down the selection.
Usually they will be such that there will be just one record
that satisfies them.
But if there are more, a single one is chosen,
by the mechanics of the built-in MongoDb command `updateOne`.
by: string
The name of the user who issued the command
Returns
-------
boolean
Whether the delete was successful
"""
criteria[MDEL] = None
updates = {MDEL: True, MDELDT: isonow(), MDELBY: by}
(good, result) = self.executeMongo(
table, "update_one", criteria, {"$set": updates}
)
return good
def undeleteRecord(self, table, criteria, by):
"""Marks a single record from a table as undeleted.
If the record was not marked as deleted, this method does silently nothing
and returns True.
Parameters
----------
table: string
The name of the table from which we want to undelete a single record.
criteria: dict
A set of criteria to narrow down the selection.
Usually they will be such that there will be just one record
that satisfies them.
But if there are more, a single one is chosen,
by the mechanics of the built-in MongoDb command `updateOne`.
by: string
The name of the user who issued the command
Returns
-------
boolean
Whether the undelete was successful
"""
criteria[MDEL] = {"$exists": True}
updates = {"$unset": {MDEL: None}, "$set": {MRESDT: isonow(), MRESBY: by}}
(good, result) = self.executeMongo(table, "update_one", criteria, updates)
return good
def hardDeleteRecords(self, table, criteria, by):
"""Delete multiple records from a table.
Parameters
----------
table: string
The name of the table from which we want to delete a records.
criteria: dict
A set of criteria to narrow down the selection.
by: string
The name of the user who issued the command
Returns
-------
boolean, integer
Whether the command completed successfully and
how many records have been deleted
"""
(good, result) = self.executeMongo(table, "delete_many", criteria)
count = result.deleted_count if good else 0
return (good, count)
def deleteRecords(self, table, criteria, by):
"""Delete multiple records from a table.
Records that have already been deleted are not affected.
Parameters
----------
table: string
The name of the table from which we want to delete a records.
criteria: dict
A set of criteria to narrow down the selection.
by: string
The name of the user who issued the command
Returns
-------
boolean, integer
Whether the command completed successfully and
how many records have been deleted
"""
criteria[MDEL] = None
updates = {MDEL: True, MDELDT: isonow(), MDELBY: by}
(good, result) = self.executeMongo(
table, "update_many", criteria, {"$set": updates}
)
count = result.modified_count if good else 0
return (good, count)
def undeleteRecords(self, table, criteria, by):
"""Marks multiple records from a table as undeleted.
Parameters
----------
table: string
The name of the table from which we want to undelete records.
criteria: dict
A set of criteria to narrow down the selection.
by: string
The name of the user who issued the command
Returns
-------
boolean, integer
Whether the command completed successfully and
how many records have been undeleted
"""
criteria[MDEL] = {"$exists": True}
updates = {"$unset": {MDEL: False}, "$set": {MRESDT: isonow(), MRESBY: by}}
(good, result) = self.executeMongo(table, "update_many", criteria, updates)
count = result.modified_count if good else 0
return (good, count)
def updateRecord(self, table, criteria, updates):
"""Updates a single record from a table.
It does not work on records that have been marked as deleted.
Parameters
----------
table: string
The name of the table in which we want to update a single record.
criteria: dict
A set of criteria to narrow down the selection.
Usually they will be such that there will be just one record
that satisfies them.
But if there are more, a single one is chosen,
by the mechanics of the built-in MongoDb command `updateOne`.
If none satisfy them, nothing is done.
updates: dict
The fields that must be updated with the values they must get.
If the value `None` is specified for a field, that field will be set to
null.
Returns
-------
boolean
Whether the update was successful
"""
criteria[MDEL] = None
(good, result) = self.executeMongo(
table, "update_one", criteria, {"$set": updates}
)
return result.modified_count > 0 if good else False
def insertRecord(self, table, fields):
"""Inserts a new record in a table.
Parameters
----------
table: string
The table in which the record will be inserted.
fields: dict
The field names and their contents to populate the new record with.
Returns
-------
ObjectId
The id of the newly inserted record, or None if the record could not be
inserted.
"""
(good, result) = self.executeMongo(table, "insert_one", dict(**fields))
return result.inserted_id if good else None
def executeMongo(self, table, command, *args, warn=True, **kwargs):
"""Executes a MongoDb command and returns the result.
Parameters
----------
table: string
The table on which to perform the command.
command: string
The built-in MongoDb command.
Note that the Python interface requires you to write camelCase commands
with underscores.
So the Mongo command `findOne` should be passed as `find_one`.
args: list
Any number of additional arguments that the command requires.
warn: boolean, optional True
If True, warn if there is an error.
kwargs: list
Any number of additional keyword arguments that the command requires.
Returns
-------
boolean, any
The `boolean` is whether an error occurred.
The `any` is whatever the MongoDb command returns.
If the command fails, an error message is issued and
`any=None` is returned.
"""
Messages = self.Messages
self.connect()
db = self.db
method = getattr(db[table], command, None)
result = None
good = True
if method is None:
if warn:
Messages.error(
msg="Database action", logmsg=f"Unknown Mongo command: `{method}`"
)
good = False
try:
result = method(*args, **kwargs)
except Exception as e:
if warn:
Messages.error(
msg="Database action",
logmsg=f"Executing Mongo command db.{table}.{command}: {e}",
)
good = False
result = None
return (good, result)
def consolidate(self, record):
"""Resolves all links in a record to title values of linked records.
The `_id` field of the record will be removed.
Values of fields with names like `xxxId` will be looked up in table `xxx`,
and will be replaced by the value of the `title` field of the found record.
Parameters
----------
record: dict or AttrDict
The record data to consolidate.
Returns
-------
dict
All AttrDict values will be recursively transformed in ordinary dict values.
"""
newRecord = AttrDict()
for k, v in record.items():
if k == "_id":
continue
if k.endswith("Id"):
table = k.removesuffix("Id")
linkedRecord = self.getRecord(table, dict(_id=v), warn=False)
v = linkedRecord.title
if v is not None:
newRecord[table] = v
else:
newRecord[k] = v
return newRecord.deepdict()
def mkBackup(self, dstBase, project=None, asJson=False):
"""Backs up data as record files in table folders.
We do site-wide backups and project-specific backups.
See also `control.backup.Backup.mkBackup`
This also backs up records that have been marked as deleted.
This function backs up database data in
[`bson`](https://www.mongodb.com/basics/bson) and/or `json` format.
Inspired by this
[gist](https://gist.github.com/Lh4cKg/939ce683e2876b314a205b3f8c6e8e9d).
Parameters
----------
dstBase: string
Destination folder.
This folder will get subfolders `bson` and/or `json` in which
the backups are stored.
project: string, optional None
If given, only backs up the given project.
asJson: boolean, optional False
Whether to create a backup in `json` format
asBson: boolean, optional True
Whether to create a backup in `bson` format
Returns
-------
boolean
Whether the operation was successful.
"""
Messages = self.Messages
self.connect()
db = self.db
tables = db.list_collection_names()
dstb = f"{dstBase}/bson"
dirMake(dstb)
dstj = None
jOpts = {}
if asJson:
dstj = f"{dstBase}/json"
dirMake(dstj)
jOpts = dict(ensure_ascii=False, indent=2, sort_keys=True)
if project is None:
for table in tables:
records = db[table].find()
n = self.writeRecords(table, records, dstb, dstj=dstj, jOpts=jOpts)
plural = "" if n == 1 else ""
Messages.info(msg=f"table {table} {n} record{plural}")
return True
(projectId, project) = self.get("project", project)
records = db.project.find(dict(_id=projectId))
n = self.writeRecords("project", records, dstb, dstj=dstj, jOpts=jOpts)
records = db.edition.find(dict(projectId=projectId))
n = self.writeRecords("edition", records, dstb, dstj=dstj, jOpts=jOpts)
return True
def writeRecords(self, table, records, dstb, dstj=None, jOpts={}):
"""Writes records to bson and possibly json file.
If the destination file already exists, it will be wiped.
Parameters
----------
table: string
Table that contains the record. Will be used as file name
for the record to be written to.
record: dict
The record as it is retrieved from MongoDb
dstb: string
Destination folder for the bson file.
dstj: string, optional None
Destination folder for the json file.
If `None`, no json file will be written.
jOpts: dict, optional {}
Format options for writing the json file.
first
Returns
-------
integer
The number of records written
"""
asJson = dstj is not None
n = 0
with open(f"{dstb}/{table}.bson", "wb") as bh:
if asJson:
jh = open(f"{dstj}/{table}.json", "w")
jh.write("[\n")
sep = ""
for record in records:
bh.write(BSON.encode(record))
n += 1
if asJson:
jh.write(sep)
jh.write(dumpjs(record, **jOpts))
sep = ",\n"
if asJson:
jh.write("\n]\n")
jh.close()
return n
def restoreBackup(self, src, project=None, clean=True):
"""Restores the database from record files in table folders.
We do site-wide restores or project-specific restores.
See also `control.backup.Backup.restoreBackup`
This also restores records that have been marked as deleted.
This function restores database data given in
[`bson`](https://www.mongodb.com/basics/bson).
Inspired by this
[gist](https://gist.github.com/Lh4cKg/939ce683e2876b314a205b3f8c6e8e9d).
Parameters
----------
src: string
Source folder.
project: string, optional None
If given, only restores the given project.
clean: boolean, optional True
Whether to delete records from a table before
restoring records to it.
If `clean=True` then, in case of site-wide restores, all records
will be cleaned. In case of project restores, only the relevant
project/edition records will be cleaned.
Returns
-------
boolean
Whether the operation was successful.
"""
Messages = self.Messages
self.connect()
db = self.db
if not dirExists(src):
Messages.warning(
msg="Source directory not found",
logmsg=f"Source directory {src} not found",
)
return False
good = True
if project is None:
with os.scandir(src) as dh:
for entry in dh:
name = entry.name
if not (entry.is_file() and name.endswith(".bson")):
continue
table = name.rsplit(".", 1)[0]
with open(f"{src}/{name}", "rb") as f:
records = decode_all(f.read())
n = len(records)
plural = "" if n == 1 else "s"
Messages.info(msg=f"table {table} {n} record{plural}")
if db[table] is not None and clean:
(thisGood, count) = self.hardDeleteRecords(
table, {}, "backuprestore"
)
if not thisGood:
good = False
db[table].insert_many(records)
return good
(projectId, project) = self.get("project", project)
with os.scandir(src) as dh:
for entry in dh:
name = entry.name
if not (entry.is_file() and name.endswith(".bson")):
continue
table = name.rsplit(".", 1)[0]
if table not in {"project", "edition"}:
continue
with open(f"{src}/{name}", "rb") as f:
records = decode_all(f.read())
thisGood = True
if table == "project":
records = [r for r in records if r._id == projectId]
nRecords = len(records)
if nRecords == 0:
Messages.warning(
msg=f"No {table} records found! Restore skipped.",
logmsg=(
f"Project restore {projectId}: "
f"No {table} records found! Skipped."
),
)
continue
elif nRecords > 1:
Messages.warning(
msg=f"Multiple {table} records found. Will restore first.",
logmsg=(
f"Project restore {projectId}: "
f"Multiple ({nRecords}) {table} records found."
),
)
record = records[0]
Messages.info(msg=f"Restoring {table} record ...")
if db[table] is not None and clean:
thisGood = self.hardDeleteRecord(table, dict(_id=projectId))
if thisGood:
db[table].insert_one(record)
else:
good = False
elif table == "edition":
records = [r for r in records if r.projectId == projectId]
nRecords = len(records)
if nRecords == 0:
Messages.info(
msg=f"No {table} records found.",
logmsg=(
f"Project restore {projectId}: "
f"No {table} records found."
),
)
continue
Messages.info(msg=f"Restoring {table} records ...")
if db[table] is not None and clean:
(thisGood, count) = self.hardDeleteRecords(
table, dict(projectId=projectId), "backuprestore"
)
if thisGood:
db[table].insert_many(records)
else:
good = False
else:
Messages.warning(
msg=f"Skipping {name} as it is not legal in a project backup",
logmsg=(
f"Skipping {name} as it is not legal in a project backup"
),
)
return good
Classes
class Mongo (Settings, Messages)
-
CRUD interface to content in the MongoDb database.
This class has methods to connect to a MongoDb database, to query its data, to create, update and delete data.
It is instantiated by a singleton object.
string versus ObjectId
Some functions execute MongoDb statements, based on parameters whose values are MongoDb identifiers. These should be objects in the class
bson.objectid.ObjectId
. However, in many cases these ids enter the app as strings.In this module, such strings will be cast to proper ObjectIds, provided they are recognizable as values in a field whose name is
_id
or ends withId
.Deletion policy
When a user deletes records, they will not be deleted, but instead they get
markedDeleted: true
. A periodic job (control.sweeper) will physically remove deleted records after 31 days. Also, deleted records will be listed on the admin interface. Admins can restore them within 30 days.See also deletion strategy.
Most methods that access the database know this policy. So if a list of records satisfying some criteria is asked, no records that are marked for deletion are returned. In other words: such records are treated as if they do not exist.
Some of these methods have an optional parameter
deleted=False
. When True, it restricts its operation on records that are marked as deleted.Some methods do not implement this policy:
executeMongo()
hardDelete…()
mkBackup()
restoreBackup()
Marking is done by adding
markedDeleted: true
to a record. Unmarking is done by removing the keymarkedDeleted
from a record.So the test whether a record
r
is marked for deletion is:r.get("markedDeleted", False)
And the mongo query expression to find unmarked records only is:
{markedDeleted: {$exists: false}}
Parameters
Settings
:AttrDict
- App-wide configuration data obtained from
Config.Settings
. Messages
:object
- Singleton instance of
Messages
.
Expand source code Browse git
class Mongo: @staticmethod def cast(value): """Try to cast the value as an ObjectId. Paramaters ---------- value:string The value to cast, normally a string representation of a BSON ObjectId. Returns ------- ObjectId | void The corresponding BSON ObjectId if the input is a valid representation of such an id, otherwise `None`. """ if value is None: return None if isinstance(value, ObjectId): return value try: oValue = ObjectId(value) except Exception: oValue = None return oValue @staticmethod def isId(value): """Test whether a value is an ObjectId Parameters ---------- value: any The value to test Returns ------- boolean Whether the value is an objectId """ return isinstance(value, ObjectId) def __init__(self, Settings, Messages): """CRUD interface to content in the MongoDb database. This class has methods to connect to a MongoDb database, to query its data, to create, update and delete data. It is instantiated by a singleton object. !!! note "string versus ObjectId" Some functions execute MongoDb statements, based on parameters whose values are MongoDb identifiers. These should be objects in the class `bson.objectid.ObjectId`. However, in many cases these ids enter the app as strings. In this module, such strings will be cast to proper ObjectIds, provided they are recognizable as values in a field whose name is `_id` or ends with `Id`. !!! note "Deletion policy" When a user deletes records, they will not be deleted, but instead they get `markedDeleted: true`. A periodic job (control.sweeper) will physically remove deleted records after 31 days. Also, deleted records will be listed on the admin interface. Admins can restore them within 30 days. See also [deletion strategy](https://github.com/CLARIAH/pure3dx/blob/main/docs/architecture.md#deletion-and-sweeping). Most methods that access the database know this policy. So if a list of records satisfying some criteria is asked, no records that are marked for deletion are returned. In other words: such records are treated as if they do not exist. Some of these methods have an optional parameter `deleted=False`. When True, it restricts its operation on records that are marked as deleted. Some methods do not implement this policy: * `executeMongo()` * `hardDelete...()` * `mkBackup()` * `restoreBackup()` Marking is done by adding `markedDeleted: true` to a record. Unmarking is done by removing the key `markedDeleted` from a record. So the test whether a record `r` is marked for deletion is: `r.get("markedDeleted", False)` And the mongo query expression to find unmarked records only is: `{markedDeleted: {$exists: false}}` Parameters ---------- Settings: AttrDict App-wide configuration data obtained from `control.config.Config.Settings`. Messages: object Singleton instance of `control.messages.Messages`. """ self.Settings = Settings runMode = Settings.runMode self.Messages = Messages Messages.debugAdd(self) self.client = None self.db = None self.database = f"{Settings.database}_{runMode}" def connect(self): """Make connection with MongoDb if there is no connection yet. The connection details come from `control.config.Config.Settings`. After a successful connection attempt, the connection handle is stored in the `client` and `db` members of the Mongo object. When a connection handle exists, this method does nothing. """ Messages = self.Messages Settings = self.Settings client = self.client db = self.db database = self.database if db is None: try: client = MongoClient( Settings.mongoHost, Settings.mongoPort, username=Settings.mongoUser, password=Settings.mongoPassword, ) db = client[database] except Exception as e: Messages.error( msg="Could not connect to the database", logmsg=f"Mongo connection: `{e}`", ) self.client = client self.db = db def disconnect(self): """Disconnect from the MongoDB.""" client = self.client if client: client.close() self.client = None self.db = None def tables(self): """List the existent tables in the database. Returns ------- list The names of the tables. """ self.connect() db = self.db return list(db.list_collection_names()) def clearTable(self, table, delete=False): """Make sure that a table exists and that it is empty. Parameters ---------- table: string The name of the table. If no such table exists, it will be created. delete: boolean, optional False If True, and the table existed before, it will be deleted. If False, the table will be cleared, i.e. all its records get deleted, but the table remains. """ Messages = self.Messages self.connect() db = self.db if delete: if db[table] is not None: try: db.drop_collection(table) Messages.plain( msg=f"dropped table `{table}`", logmsg=f"dropped table `{table}`", ) except Exception as e: Messages.error( msg="Database action", logmsg=f"Cannot delete table: `{table}`: {e}", ) else: if db[table] is None: try: db.create_collection(table) except Exception as e: Messages.error( msg="Database action", logmsg=f"Cannot create table: `{table}`: {e}", ) else: (good, count) = self.deleteRecordsHard(table, {}, "system") if good: plural = "" if count == 1 else "s" Messages.plain( msg=f"cleared table `{table} of {count} record{plural}`", logmsg=f"cleared table `{table} of {count} record{plural}`", ) def get(self, table, record, deleted=False): """Get the record and recordId if only one of them is specified. If the record is specified by id, the id maybe an ObjectId or a string, which will then be cast to an ObjectId. Parameters ---------- table: string The table in which the record can be found record: string | ObjectID | AttrDict | void Either the id of the record, or the record itself. deleted: boolean, optional False Search only in the records that are marked for deletion Returns ------- tuple * ObjectId: the id of the record * AttrDict: the record itself If `record` is None, both members of the tuple are None """ if record is None: return (None, None) if type(record) is str: record = self.getRecord(table, dict(_id=self.cast(record)), deleted=deleted) elif self.isId(record): record = self.getRecord(table, dict(_id=record), deleted=deleted) recordId = record._id return (recordId, record) def getRecord(self, table, criteria, deleted=False, warn=True): """Get a single record from a table. Parameters ---------- table: string The name of the table from which we want to retrieve a single record. criteria: dict A set of criteria to narrow down the search. Usually they will be such that there will be just one record that satisfies them. But if there are more, a single one is chosen, by the mechanics of the built-in MongoDb command `findOne`. deleted: boolean, optional False Search only in the records that are marked for deletion warn: boolean, optional True If True, warn if there is no record satisfying the criteria. Returns ------- AttrDict The single record found, or an empty AttrDict if no record satisfies the criteria. """ Messages = self.Messages criteria[MDEL] = {"$exists": True} if deleted else None (good, result) = self.executeMongo(table, "find_one", criteria, {}, warn=False) if not good or result is None: if warn: Messages.warning( msg=f"Could not find that {table}", logmsg=f"No record in {table} with {criteria}", ) result = {} return deepAttrDict(result) def getList(self, table, criteria, deleted=False, sort=None, asDict=False): """Get a list of records from a table. Parameters ---------- table: string The name of the table from which we want to retrieve records. criteria: dict A set of criteria to narrow down the search. deleted: boolean, optional False Search only in the records that are marked for deletion sort: string | function, optional None Sort key. If `None`, the results will not be sorted. If a string, it is the name of a field by which the results will be sorted in ascending order. If a function, the function should take a record as input and return a value. The records will be sorted by this value. asDict: boolean or string, optional False If False, returns a list of records as result. If True or a string, returns the same records, but now as dict, keyed by the `_id` field if asDict is True, else keyed by the field in dictated by asDict. Returns ------- list of AttrDict The list of records found, empty if no records are found. Each record is cast to an AttrDict. """ criteria[MDEL] = {"$exists": True} if deleted else None (good, result) = self.executeMongo(table, "find", criteria, {}) if not good: return [] unsorted = [deepAttrDict(record) for record in result] if sort is None: result = unsorted else: sortFunc = (lambda r: r[sort] or "") if type(sort) is str else sort result = sorted(unsorted, key=sortFunc) return ( {r[asDict]: r for r in result} if type(asDict) is str else {r._id: r for r in result} if asDict else result ) def hardDeleteRecord(self, table, criteria, by): """Deletes a single record from a table. Parameters ---------- table: string The name of the table from which we want to delete a single record. criteria: dict A set of criteria to narrow down the selection. Usually they will be such that there will be just one record that satisfies them. But if there are more, a single one is chosen, by the mechanics of the built-in MongoDb command `updateOne`. by: string The name of the user who issued the command Returns ------- boolean Whether the delete was successful """ (good, result) = self.executeMongo(table, "delete_one", criteria) return result.deleted_count > 0 if good else False def deleteRecord(self, table, criteria, by): """Deletes a single record from a table. If the record has already been deleted, nothing is done. Parameters ---------- table: string The name of the table from which we want to delete a single record. criteria: dict A set of criteria to narrow down the selection. Usually they will be such that there will be just one record that satisfies them. But if there are more, a single one is chosen, by the mechanics of the built-in MongoDb command `updateOne`. by: string The name of the user who issued the command Returns ------- boolean Whether the delete was successful """ criteria[MDEL] = None updates = {MDEL: True, MDELDT: isonow(), MDELBY: by} (good, result) = self.executeMongo( table, "update_one", criteria, {"$set": updates} ) return good def undeleteRecord(self, table, criteria, by): """Marks a single record from a table as undeleted. If the record was not marked as deleted, this method does silently nothing and returns True. Parameters ---------- table: string The name of the table from which we want to undelete a single record. criteria: dict A set of criteria to narrow down the selection. Usually they will be such that there will be just one record that satisfies them. But if there are more, a single one is chosen, by the mechanics of the built-in MongoDb command `updateOne`. by: string The name of the user who issued the command Returns ------- boolean Whether the undelete was successful """ criteria[MDEL] = {"$exists": True} updates = {"$unset": {MDEL: None}, "$set": {MRESDT: isonow(), MRESBY: by}} (good, result) = self.executeMongo(table, "update_one", criteria, updates) return good def hardDeleteRecords(self, table, criteria, by): """Delete multiple records from a table. Parameters ---------- table: string The name of the table from which we want to delete a records. criteria: dict A set of criteria to narrow down the selection. by: string The name of the user who issued the command Returns ------- boolean, integer Whether the command completed successfully and how many records have been deleted """ (good, result) = self.executeMongo(table, "delete_many", criteria) count = result.deleted_count if good else 0 return (good, count) def deleteRecords(self, table, criteria, by): """Delete multiple records from a table. Records that have already been deleted are not affected. Parameters ---------- table: string The name of the table from which we want to delete a records. criteria: dict A set of criteria to narrow down the selection. by: string The name of the user who issued the command Returns ------- boolean, integer Whether the command completed successfully and how many records have been deleted """ criteria[MDEL] = None updates = {MDEL: True, MDELDT: isonow(), MDELBY: by} (good, result) = self.executeMongo( table, "update_many", criteria, {"$set": updates} ) count = result.modified_count if good else 0 return (good, count) def undeleteRecords(self, table, criteria, by): """Marks multiple records from a table as undeleted. Parameters ---------- table: string The name of the table from which we want to undelete records. criteria: dict A set of criteria to narrow down the selection. by: string The name of the user who issued the command Returns ------- boolean, integer Whether the command completed successfully and how many records have been undeleted """ criteria[MDEL] = {"$exists": True} updates = {"$unset": {MDEL: False}, "$set": {MRESDT: isonow(), MRESBY: by}} (good, result) = self.executeMongo(table, "update_many", criteria, updates) count = result.modified_count if good else 0 return (good, count) def updateRecord(self, table, criteria, updates): """Updates a single record from a table. It does not work on records that have been marked as deleted. Parameters ---------- table: string The name of the table in which we want to update a single record. criteria: dict A set of criteria to narrow down the selection. Usually they will be such that there will be just one record that satisfies them. But if there are more, a single one is chosen, by the mechanics of the built-in MongoDb command `updateOne`. If none satisfy them, nothing is done. updates: dict The fields that must be updated with the values they must get. If the value `None` is specified for a field, that field will be set to null. Returns ------- boolean Whether the update was successful """ criteria[MDEL] = None (good, result) = self.executeMongo( table, "update_one", criteria, {"$set": updates} ) return result.modified_count > 0 if good else False def insertRecord(self, table, fields): """Inserts a new record in a table. Parameters ---------- table: string The table in which the record will be inserted. fields: dict The field names and their contents to populate the new record with. Returns ------- ObjectId The id of the newly inserted record, or None if the record could not be inserted. """ (good, result) = self.executeMongo(table, "insert_one", dict(**fields)) return result.inserted_id if good else None def executeMongo(self, table, command, *args, warn=True, **kwargs): """Executes a MongoDb command and returns the result. Parameters ---------- table: string The table on which to perform the command. command: string The built-in MongoDb command. Note that the Python interface requires you to write camelCase commands with underscores. So the Mongo command `findOne` should be passed as `find_one`. args: list Any number of additional arguments that the command requires. warn: boolean, optional True If True, warn if there is an error. kwargs: list Any number of additional keyword arguments that the command requires. Returns ------- boolean, any The `boolean` is whether an error occurred. The `any` is whatever the MongoDb command returns. If the command fails, an error message is issued and `any=None` is returned. """ Messages = self.Messages self.connect() db = self.db method = getattr(db[table], command, None) result = None good = True if method is None: if warn: Messages.error( msg="Database action", logmsg=f"Unknown Mongo command: `{method}`" ) good = False try: result = method(*args, **kwargs) except Exception as e: if warn: Messages.error( msg="Database action", logmsg=f"Executing Mongo command db.{table}.{command}: {e}", ) good = False result = None return (good, result) def consolidate(self, record): """Resolves all links in a record to title values of linked records. The `_id` field of the record will be removed. Values of fields with names like `xxxId` will be looked up in table `xxx`, and will be replaced by the value of the `title` field of the found record. Parameters ---------- record: dict or AttrDict The record data to consolidate. Returns ------- dict All AttrDict values will be recursively transformed in ordinary dict values. """ newRecord = AttrDict() for k, v in record.items(): if k == "_id": continue if k.endswith("Id"): table = k.removesuffix("Id") linkedRecord = self.getRecord(table, dict(_id=v), warn=False) v = linkedRecord.title if v is not None: newRecord[table] = v else: newRecord[k] = v return newRecord.deepdict() def mkBackup(self, dstBase, project=None, asJson=False): """Backs up data as record files in table folders. We do site-wide backups and project-specific backups. See also `control.backup.Backup.mkBackup` This also backs up records that have been marked as deleted. This function backs up database data in [`bson`](https://www.mongodb.com/basics/bson) and/or `json` format. Inspired by this [gist](https://gist.github.com/Lh4cKg/939ce683e2876b314a205b3f8c6e8e9d). Parameters ---------- dstBase: string Destination folder. This folder will get subfolders `bson` and/or `json` in which the backups are stored. project: string, optional None If given, only backs up the given project. asJson: boolean, optional False Whether to create a backup in `json` format asBson: boolean, optional True Whether to create a backup in `bson` format Returns ------- boolean Whether the operation was successful. """ Messages = self.Messages self.connect() db = self.db tables = db.list_collection_names() dstb = f"{dstBase}/bson" dirMake(dstb) dstj = None jOpts = {} if asJson: dstj = f"{dstBase}/json" dirMake(dstj) jOpts = dict(ensure_ascii=False, indent=2, sort_keys=True) if project is None: for table in tables: records = db[table].find() n = self.writeRecords(table, records, dstb, dstj=dstj, jOpts=jOpts) plural = "" if n == 1 else "" Messages.info(msg=f"table {table} {n} record{plural}") return True (projectId, project) = self.get("project", project) records = db.project.find(dict(_id=projectId)) n = self.writeRecords("project", records, dstb, dstj=dstj, jOpts=jOpts) records = db.edition.find(dict(projectId=projectId)) n = self.writeRecords("edition", records, dstb, dstj=dstj, jOpts=jOpts) return True def writeRecords(self, table, records, dstb, dstj=None, jOpts={}): """Writes records to bson and possibly json file. If the destination file already exists, it will be wiped. Parameters ---------- table: string Table that contains the record. Will be used as file name for the record to be written to. record: dict The record as it is retrieved from MongoDb dstb: string Destination folder for the bson file. dstj: string, optional None Destination folder for the json file. If `None`, no json file will be written. jOpts: dict, optional {} Format options for writing the json file. first Returns ------- integer The number of records written """ asJson = dstj is not None n = 0 with open(f"{dstb}/{table}.bson", "wb") as bh: if asJson: jh = open(f"{dstj}/{table}.json", "w") jh.write("[\n") sep = "" for record in records: bh.write(BSON.encode(record)) n += 1 if asJson: jh.write(sep) jh.write(dumpjs(record, **jOpts)) sep = ",\n" if asJson: jh.write("\n]\n") jh.close() return n def restoreBackup(self, src, project=None, clean=True): """Restores the database from record files in table folders. We do site-wide restores or project-specific restores. See also `control.backup.Backup.restoreBackup` This also restores records that have been marked as deleted. This function restores database data given in [`bson`](https://www.mongodb.com/basics/bson). Inspired by this [gist](https://gist.github.com/Lh4cKg/939ce683e2876b314a205b3f8c6e8e9d). Parameters ---------- src: string Source folder. project: string, optional None If given, only restores the given project. clean: boolean, optional True Whether to delete records from a table before restoring records to it. If `clean=True` then, in case of site-wide restores, all records will be cleaned. In case of project restores, only the relevant project/edition records will be cleaned. Returns ------- boolean Whether the operation was successful. """ Messages = self.Messages self.connect() db = self.db if not dirExists(src): Messages.warning( msg="Source directory not found", logmsg=f"Source directory {src} not found", ) return False good = True if project is None: with os.scandir(src) as dh: for entry in dh: name = entry.name if not (entry.is_file() and name.endswith(".bson")): continue table = name.rsplit(".", 1)[0] with open(f"{src}/{name}", "rb") as f: records = decode_all(f.read()) n = len(records) plural = "" if n == 1 else "s" Messages.info(msg=f"table {table} {n} record{plural}") if db[table] is not None and clean: (thisGood, count) = self.hardDeleteRecords( table, {}, "backuprestore" ) if not thisGood: good = False db[table].insert_many(records) return good (projectId, project) = self.get("project", project) with os.scandir(src) as dh: for entry in dh: name = entry.name if not (entry.is_file() and name.endswith(".bson")): continue table = name.rsplit(".", 1)[0] if table not in {"project", "edition"}: continue with open(f"{src}/{name}", "rb") as f: records = decode_all(f.read()) thisGood = True if table == "project": records = [r for r in records if r._id == projectId] nRecords = len(records) if nRecords == 0: Messages.warning( msg=f"No {table} records found! Restore skipped.", logmsg=( f"Project restore {projectId}: " f"No {table} records found! Skipped." ), ) continue elif nRecords > 1: Messages.warning( msg=f"Multiple {table} records found. Will restore first.", logmsg=( f"Project restore {projectId}: " f"Multiple ({nRecords}) {table} records found." ), ) record = records[0] Messages.info(msg=f"Restoring {table} record ...") if db[table] is not None and clean: thisGood = self.hardDeleteRecord(table, dict(_id=projectId)) if thisGood: db[table].insert_one(record) else: good = False elif table == "edition": records = [r for r in records if r.projectId == projectId] nRecords = len(records) if nRecords == 0: Messages.info( msg=f"No {table} records found.", logmsg=( f"Project restore {projectId}: " f"No {table} records found." ), ) continue Messages.info(msg=f"Restoring {table} records ...") if db[table] is not None and clean: (thisGood, count) = self.hardDeleteRecords( table, dict(projectId=projectId), "backuprestore" ) if thisGood: db[table].insert_many(records) else: good = False else: Messages.warning( msg=f"Skipping {name} as it is not legal in a project backup", logmsg=( f"Skipping {name} as it is not legal in a project backup" ), ) return good
Static methods
def cast(value)
-
Try to cast the value as an ObjectId. Paramaters
value:string The value to cast, normally a string representation of a BSON ObjectId.
Returns
ObjectId | void
- The corresponding BSON ObjectId if the input is a valid representation of
such an id, otherwise
None
.
def isId(value)
-
Test whether a value is an ObjectId
Parameters
value
:any
The value to test
Returns
boolean
- Whether the value is an objectId
Methods
def clearTable(self, table, delete=False)
-
Make sure that a table exists and that it is empty.
Parameters
table
:string
- The name of the table. If no such table exists, it will be created.
delete
:boolean
, optionalFalse
- If True, and the table existed before, it will be deleted. If False, the table will be cleared, i.e. all its records get deleted, but the table remains.
def connect(self)
-
Make connection with MongoDb if there is no connection yet.
The connection details come from
Config.Settings
.After a successful connection attempt, the connection handle is stored in the
client
anddb
members of the Mongo object.When a connection handle exists, this method does nothing.
def consolidate(self, record)
-
Resolves all links in a record to title values of linked records.
The
_id
field of the record will be removed. Values of fields with names likexxxId
will be looked up in tablexxx
, and will be replaced by the value of thetitle
field of the found record.Parameters
record
:dict
orAttrDict
- The record data to consolidate.
Returns
dict
- All AttrDict values will be recursively transformed in ordinary dict values.
def deleteRecord(self, table, criteria, by)
-
Deletes a single record from a table.
If the record has already been deleted, nothing is done.
Parameters
table
:string
- The name of the table from which we want to delete a single record.
criteria
:dict
- A set of criteria to narrow down the selection.
Usually they will be such that there will be just one record
that satisfies them.
But if there are more, a single one is chosen,
by the mechanics of the built-in MongoDb command
updateOne
. by
:string
- The name of the user who issued the command
Returns
boolean
- Whether the delete was successful
def deleteRecords(self, table, criteria, by)
-
Delete multiple records from a table.
Records that have already been deleted are not affected.
Parameters
table
:string
- The name of the table from which we want to delete a records.
criteria
:dict
- A set of criteria to narrow down the selection.
by
:string
- The name of the user who issued the command
Returns
boolean, integer
- Whether the command completed successfully and how many records have been deleted
def disconnect(self)
-
Disconnect from the MongoDB.
def executeMongo(self, table, command, *args, warn=True, **kwargs)
-
Executes a MongoDb command and returns the result.
Parameters
table
:string
- The table on which to perform the command.
command
:string
- The built-in MongoDb command.
Note that the Python interface requires you to write camelCase commands
with underscores.
So the Mongo command
findOne
should be passed asfind_one
. args
:list
- Any number of additional arguments that the command requires.
warn
:boolean
, optionalTrue
- If True, warn if there is an error.
kwargs
:list
- Any number of additional keyword arguments that the command requires.
Returns
boolean, any
-
The
boolean
is whether an error occurred.The
any
is whatever the MongoDb command returns. If the command fails, an error message is issued andany=None
is returned.
def get(self, table, record, deleted=False)
-
Get the record and recordId if only one of them is specified.
If the record is specified by id, the id maybe an ObjectId or a string, which will then be cast to an ObjectId.
Parameters
table
:string
- The table in which the record can be found
record
:string | ObjectID | AttrDict | void
- Either the id of the record, or the record itself.
deleted
:boolean
, optionalFalse
- Search only in the records that are marked for deletion
Returns
tuple
-
- ObjectId: the id of the record
- AttrDict: the record itself
If
record
is None, both members of the tuple are None
def getList(self, table, criteria, deleted=False, sort=None, asDict=False)
-
Get a list of records from a table.
Parameters
table
:string
- The name of the table from which we want to retrieve records.
criteria
:dict
- A set of criteria to narrow down the search.
deleted
:boolean
, optionalFalse
- Search only in the records that are marked for deletion
sort
:string | function
, optionalNone
- Sort key. If
None
, the results will not be sorted. If a string, it is the name of a field by which the results will be sorted in ascending order. If a function, the function should take a record as input and return a value. The records will be sorted by this value. asDict
:boolean
orstring
, optionalFalse
- If False, returns a list of records as result. If True or a string, returns
the same records, but now as dict, keyed by the
_id
field if asDict is True, else keyed by the field in dictated by asDict.
Returns
list
ofAttrDict
- The list of records found, empty if no records are found. Each record is cast to an AttrDict.
def getRecord(self, table, criteria, deleted=False, warn=True)
-
Get a single record from a table.
Parameters
table
:string
- The name of the table from which we want to retrieve a single record.
criteria
:dict
- A set of criteria to narrow down the search.
Usually they will be such that there will be just one record
that satisfies them.
But if there are more, a single one is chosen,
by the mechanics of the built-in MongoDb command
findOne
. deleted
:boolean
, optionalFalse
- Search only in the records that are marked for deletion
warn
:boolean
, optionalTrue
- If True, warn if there is no record satisfying the criteria.
Returns
AttrDict
- The single record found, or an empty AttrDict if no record satisfies the criteria.
def hardDeleteRecord(self, table, criteria, by)
-
Deletes a single record from a table.
Parameters
table
:string
- The name of the table from which we want to delete a single record.
criteria
:dict
- A set of criteria to narrow down the selection.
Usually they will be such that there will be just one record
that satisfies them.
But if there are more, a single one is chosen,
by the mechanics of the built-in MongoDb command
updateOne
. by
:string
- The name of the user who issued the command
Returns
boolean
- Whether the delete was successful
def hardDeleteRecords(self, table, criteria, by)
-
Delete multiple records from a table.
Parameters
table
:string
- The name of the table from which we want to delete a records.
criteria
:dict
- A set of criteria to narrow down the selection.
by
:string
- The name of the user who issued the command
Returns
boolean, integer
- Whether the command completed successfully and how many records have been deleted
def insertRecord(self, table, fields)
-
Inserts a new record in a table.
Parameters
table
:string
- The table in which the record will be inserted.
fields
:dict
- The field names and their contents to populate the new record with.
Returns
ObjectId
- The id of the newly inserted record, or None if the record could not be inserted.
def mkBackup(self, dstBase, project=None, asJson=False)
-
Backs up data as record files in table folders.
We do site-wide backups and project-specific backups.
See also
Backup.mkBackup()
This also backs up records that have been marked as deleted.
This function backs up database data in
bson
and/orjson
format.Inspired by this gist.
Parameters
dstBase
:string
- Destination folder.
This folder will get subfolders
bson
and/orjson
in which the backups are stored. project
:string
, optionalNone
- If given, only backs up the given project.
asJson
:boolean
, optionalFalse
- Whether to create a backup in
json
format asBson
:boolean
, optionalTrue
- Whether to create a backup in
bson
format
Returns
boolean
- Whether the operation was successful.
def restoreBackup(self, src, project=None, clean=True)
-
Restores the database from record files in table folders.
We do site-wide restores or project-specific restores.
See also
Backup.restoreBackup()
This also restores records that have been marked as deleted.
This function restores database data given in
bson
.Inspired by this gist.
Parameters
src
:string
- Source folder.
project
:string
, optionalNone
- If given, only restores the given project.
clean
:boolean
, optionalTrue
- Whether to delete records from a table before
restoring records to it.
If
clean=True
then, in case of site-wide restores, all records will be cleaned. In case of project restores, only the relevant project/edition records will be cleaned.
Returns
boolean
- Whether the operation was successful.
def tables(self)
-
List the existent tables in the database.
Returns
list
- The names of the tables.
def undeleteRecord(self, table, criteria, by)
-
Marks a single record from a table as undeleted.
If the record was not marked as deleted, this method does silently nothing and returns True.
Parameters
table
:string
- The name of the table from which we want to undelete a single record.
criteria
:dict
- A set of criteria to narrow down the selection.
Usually they will be such that there will be just one record
that satisfies them.
But if there are more, a single one is chosen,
by the mechanics of the built-in MongoDb command
updateOne
. by
:string
- The name of the user who issued the command
Returns
boolean
- Whether the undelete was successful
def undeleteRecords(self, table, criteria, by)
-
Marks multiple records from a table as undeleted.
Parameters
table
:string
- The name of the table from which we want to undelete records.
criteria
:dict
- A set of criteria to narrow down the selection.
by
:string
- The name of the user who issued the command
Returns
boolean, integer
- Whether the command completed successfully and how many records have been undeleted
def updateRecord(self, table, criteria, updates)
-
Updates a single record from a table.
It does not work on records that have been marked as deleted.
Parameters
table
:string
- The name of the table in which we want to update a single record.
criteria
:dict
- A set of criteria to narrow down the selection.
Usually they will be such that there will be just one record
that satisfies them.
But if there are more, a single one is chosen,
by the mechanics of the built-in MongoDb command
updateOne
. If none satisfy them, nothing is done. updates
:dict
- The fields that must be updated with the values they must get.
If the value
None
is specified for a field, that field will be set to null.
Returns
boolean
- Whether the update was successful
def writeRecords(self, table, records, dstb, dstj=None, jOpts={})
-
Writes records to bson and possibly json file.
If the destination file already exists, it will be wiped.
Parameters
table
:string
- Table that contains the record. Will be used as file name for the record to be written to.
record
:dict
- The record as it is retrieved from MongoDb
dstb
:string
- Destination folder for the bson file.
dstj
:string
, optionalNone
- Destination folder for the json file.
If
None
, no json file will be written. jOpts
:dict
, optional{}
- Format options for writing the json file.
first
Returns
integer
- The number of records written