From 86f7eccdaf453aea1f4c7e1b7d0b081e3137e942 Mon Sep 17 00:00:00 2001 From: bbm Date: Thu, 26 Mar 2026 15:18:47 -0400 Subject: [PATCH 1/8] move webquery logic from slddb server code to orsopy.sldddb --- orsopy/slddb/webapi.py | 109 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/orsopy/slddb/webapi.py b/orsopy/slddb/webapi.py index 6facd54..59ffac6 100644 --- a/orsopy/slddb/webapi.py +++ b/orsopy/slddb/webapi.py @@ -190,3 +190,112 @@ def bio_blender(self, sequence, molecule="protein"): out = Material(Formula(res["formula"]), **mat_data) return out + + +# webquery API functions: +def calc_api(args): + """Calculate SLD from formula/density or biological sequence. + + args: dict-like with optional keys: formula, density, protein, dna, rna, + name, material_description, xray_unit. + Returns a JSON string. + """ + if 'protein' in args: + try: + material = collect_protein(args['protein']) + except Exception as e: + return repr(e) + else: + name = args.get('name', 'protein') + elif 'dna' in args: + try: + material = collect_dna(args['dna']) + except Exception as e: + return repr(e) + else: + name = args.get('name', 'DNA') + elif 'rna' in args: + try: + material = collect_rna(args['rna']) + except Exception as e: + return repr(e) + else: + name = args.get('name', 'RNA') + elif 'formula' in args and 'density' in args: + f = Formula(args['formula'], sort=False) + try: + material = Material(f, dens=float(args['density'])) + except Exception as e: + return repr(e) + else: + name = args.get('name', 'User Query') + else: + return 'Could not calculate, missing formula and density or protein/dna/rna sequence' + material.name = name + if args.get('material_description', '') != '': + material.extra_data['description'] = args['material_description'] + out = material.export(xray_units=args.get('xray_unit', 'edens')) + return out + + +def select_api(args): + """Return JSON for a material selected by ID. + + args: dict-like with keys: ID, and optionally xray_unit. + Returns a JSON string. + """ + db = SLDDB(DB_FILE) + res = db.search_material(filter_invalid=False, ID=int(args['ID'])) + try: + material = db.select_material(res[0]) + except IndexError: + return '## ID not found in database' + except Exception as e: + return repr(e) + '
' + "Raised when tried to parse material = %s" % res[0] + out = material.export(xray_units=args.get('xray_unit', 'edens')) + return out + +def search_api(args): + """Search the database with the given field values. + + args: dict-like mapping DB field names to query values. + Returns a JSON string. + """ + query = {} + for key, value in args.items(): + if str(value).strip() == '': + continue + if key in DB_MATERIALS_FIELDS: + try: + query[key] = db_lookup[key][1].convert(str(value)) + except Exception as e: + return repr(e) + '
' + "Raised when tried to parse %s = %s" % (key, value) + db = SLDDB(DB_FILE) + res = db.search_material(serializable=True, limit=10000, **query) + + # remove hidden database fields besides ORSO validation + for ri in res: + for field in DB_MATERIALS_HIDDEN_DATA: + if field.startswith('validated'): + continue + del ri[field] + + return res + + +def query_api(args): + """Dispatch an API request based on which keys are present in args. + + args: dict-like (e.g. request.args or a plain dict). + Returns a JSON string. + """ + if 'ID' in args: + return select_api(args) + elif 'sldcalc' in args: + return calc_api(args) + elif 'get_fields' in args: + return [ + field for field in DB_MATERIALS_FIELDS if field not in DB_MATERIALS_HIDDEN_DATA + ] + else: + return search_api(args) \ No newline at end of file From 5c32376a909a5009fe3a9ea8b694563ebf1f868e Mon Sep 17 00:00:00 2001 From: bbm Date: Thu, 26 Mar 2026 15:19:15 -0400 Subject: [PATCH 2/8] add support for blender directly in orsopy.slddb (from slddb server code) --- orsopy/slddb/blender.py | 136 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 orsopy/slddb/blender.py diff --git a/orsopy/slddb/blender.py b/orsopy/slddb/blender.py new file mode 100644 index 0000000..ef1a4c7 --- /dev/null +++ b/orsopy/slddb/blender.py @@ -0,0 +1,136 @@ +""" +Pure calculation functions for combining biological sequences and material blends. +No web-framework dependencies. +""" + +from .dbconfig import DB_FILE +from .database import SLDDB +from .material import Material +from .element_table import get_element +from .comparators import ExactString + +AMINO_ABRV = { + "A": "Alanine", + "R": "Arginine", + "N": "Asparagine", + "D": "Aspartate", + "B": "Aspartate", + "C": "Cysteine", + "E": "Glutamate", + "Q": "Glutamine", + "Z": "Glutamate", + "G": "Glycine", + "H": "Histidine", + "I": "Isoleucine", + "L": "Leucine", + "K": "Lysine", + "M": "Methionine", + "F": "Phenylalanine", + "P": "Proline", + "S": "Serine", + "T": "Threonine", + "W": "Tryptophan", + "Y": "Tyrosine", + "V": "Valine", +} + +RNA_ABRV = { + "A": "RNA-Adenine", + "G": "RNA-Guanine", + "C": "RNA-Cytosine", + "U": "RNA-Uracil", +} + +DNA_ABRV = { + "A": "DNA-Adenine", + "G": "DNA-Guanine", + "C": "DNA-Cytosine", + "T": "DNA-Thymine", +} + + +class SequenceParseError(ValueError): + pass + + +def clean_str(string): + return string.replace('\n', '').replace('\r', '').replace('\t', '').replace(' ', '').strip() + + +hx2o = Material([(get_element(element), amount) for element, amount in [('Hx', 2.0), ('O', 1.0)]], dens=1.0) + + +def collect_combination(ids, name_dict): + db = SLDDB(DB_FILE) + elements: list[Material] = [] + loaded_ids: dict[str, Material] = {} + for id in ids: + if not id in loaded_ids: + try: + entry = db.search_material(name=ExactString(name_dict[id]))[0] + except KeyError: + possible_ids = name_dict.keys() + raise SequenceParseError(f"Not a valid identifier {id}, options are {''.join(possible_ids)}") + except IndexError: + raise SequenceParseError(f"Molecule {name_dict[id]} not found in database") + m = db.select_material(entry) + loaded_ids[id] = m + elements.append(loaded_ids[id]) + result = elements[0] + for element in elements[1:]: + result += element + return result + + +def collect_protein(acids): + acids = clean_str(acids).upper() + result = collect_combination(acids, AMINO_ABRV) + hx2o + result.extra_data['description'] = f'protein - {len(acids)} residues' + return result + + +def collect_dna(bases): + bases = clean_str(bases).upper() + result = collect_combination(bases, DNA_ABRV) + hx2o + result.extra_data['description'] = f'DNA - {len(bases)} residues' + return result + + +def collect_rna(bases): + bases = clean_str(bases).upper() + result = collect_combination(bases, RNA_ABRV) + hx2o + result.extra_data['description'] = f'RNA - {len(bases)} residues' + return result + + +def collect_blendIDs(formula): + db = SLDDB(DB_FILE) + elements: list[Material] = [] + loaded_ids = {} + items = [] + while '(' in clean_str(formula): + pre, formula = formula.split(')', 1) + number = float(pre.split('*', 1)[0].strip('(').strip()) + ID = int(pre.split('*', 1)[1].strip()) + items.append((number, ID)) + for number, ID in items: + if not ID in loaded_ids: + entry = db.search_material(ID=ID)[0] + m = db.select_material(entry) + loaded_ids[ID] = m + elements.append(number * loaded_ids[ID]) + result = elements[0] + for element in elements[1:]: + result += element + return result + + +def collect_blend(mtype, idstr): + if mtype == 'protein': + return collect_protein(idstr) + elif mtype == 'dna': + return collect_dna(idstr) + elif mtype == 'rna': + return collect_rna(idstr) + elif mtype == 'db': + return collect_blendIDs(idstr) From b20d0d06e2db85aa718316feb8fecfd875685041 Mon Sep 17 00:00:00 2001 From: bbm Date: Thu, 26 Mar 2026 15:21:52 -0400 Subject: [PATCH 3/8] make updating the local db optional with SLD_API(update_db=False) --- orsopy/slddb/webapi.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/orsopy/slddb/webapi.py b/orsopy/slddb/webapi.py index 59ffac6..76b799b 100644 --- a/orsopy/slddb/webapi.py +++ b/orsopy/slddb/webapi.py @@ -46,13 +46,13 @@ class SLD_API: max_age = 1 db: SLDDB = None - def __init__(self): - self.first_access = True - self.use_webquery = True # only try webquery once, if error occurs switch to local database + def __init__(self, update_db=True): + self.db_needs_update = update_db + self.use_webquery = False # default to using local database, which is updated regularly def check(self): # make sure the local database file is up to date, if not try to download newest version - if self.first_access: + if self.db_needs_update: now = datetime.datetime.now() try: stat = pathlib.Path(DB_FILE).stat() @@ -70,7 +70,7 @@ def check(self): except URLError as err: warnings.warn("Can't download new version of database; " + str(err)) self.db = SLDDB(DB_FILE) # after potential update, make connection with local database - self.first_access = False + self.db_needs_update = False else: return From 27ada1a5059bfdf4b3d5af43ed88b871a236a312 Mon Sep 17 00:00:00 2001 From: bbm Date: Thu, 26 Mar 2026 15:22:24 -0400 Subject: [PATCH 4/8] update imports to support new webquery local API + blender --- orsopy/slddb/webapi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/orsopy/slddb/webapi.py b/orsopy/slddb/webapi.py index 76b799b..e962e74 100644 --- a/orsopy/slddb/webapi.py +++ b/orsopy/slddb/webapi.py @@ -9,7 +9,8 @@ from urllib.error import URLError from . import DB_FILE, SLDDB -from .dbconfig import WEBAPI_URL +from .dbconfig import WEBAPI_URL, DB_MATERIALS_FIELDS, DB_MATERIALS_HIDDEN_DATA, db_lookup +from .blender import collect_protein, collect_dna, collect_rna from .element_table import get_element from .material import Formula, Material From 10edca90b9a211fc8aca9db09eb7af527d2aaa81 Mon Sep 17 00:00:00 2001 From: bbm Date: Thu, 26 Mar 2026 15:23:11 -0400 Subject: [PATCH 5/8] use SLD_API.search(query_dict) in all functions, uses webquery or local based on settings --- orsopy/slddb/webapi.py | 45 ++++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/orsopy/slddb/webapi.py b/orsopy/slddb/webapi.py index e962e74..65a1898 100644 --- a/orsopy/slddb/webapi.py +++ b/orsopy/slddb/webapi.py @@ -96,11 +96,7 @@ def webquery(qdict): return json.loads(webdata.read()) # return decoded data def localquery(self, qdict): - return self.db.search_material(**qdict) - - def localmaterial(self, ID): - res = self.db.search_material(ID=ID) - return self.db.select_material(res[0]) + return query_api(qdict) def search(self, **opts): """ @@ -131,29 +127,22 @@ def material(self, ID): material=api.material(res[0]['ID']) print(material.dens, material.rho_n, material.f_of_E(8.0)) """ - if not self.use_webquery: - return self.localmaterial(ID) - self.check() - try: - res = self.webquery({"ID": int(ID)}) - except URLError: - self.use_webquery = False - return self.localmaterial(ID) - else: - f = Formula(res["formula"], sort=False) - mat_data = dict(dens=float(res["density"]), ID=ID, extra_data={}) - if res.get("name", None): - mat_data["name"] = res["name"] - if res.get("mu", 0.0): - mat_data["mu"] = res["mu"] - elif res.get("M", 0.0): - mat_data["M"] = res["M"] - for key in ["ORSO_validated", "description", "doi", "reference"]: - if key in res: - mat_data["extra_data"][key] = res[key] - out = Material([(get_element(element), amount) for element, amount in f], **mat_data) - return out + res = self.search(ID=int(ID)) + + f = Formula(res["formula"], sort=False) + mat_data = dict(dens=float(res["density"]), ID=ID, extra_data={}) + if res.get("name", None): + mat_data["name"] = res["name"] + if res.get("mu", 0.0): + mat_data["mu"] = res["mu"] + elif res.get("M", 0.0): + mat_data["M"] = res["M"] + for key in ["ORSO_validated", "description", "doi", "reference"]: + if key in res: + mat_data["extra_data"][key] = res[key] + out = Material([(get_element(element), amount) for element, amount in f], **mat_data) + return out @staticmethod def custom(formula, dens=None, fu_volume=None, rho_n=None, mu=0.0, xsld=None, xE=None): @@ -181,7 +170,7 @@ def bio_blender(self, sequence, molecule="protein"): Get material for protein, DNA or RNA. Provide a letter sequence and molecule type ('protein', 'dna', 'rna'). """ opts = {molecule.lower(): sequence, "sldcalc": "true"} - res = self.webquery(opts) + res = self.search(**opts) mat_data = dict(fu_volume=float(res["fu_volume"]), name=f"BioBlender-{molecule.lower()}", extra_data={}) for key in [ "description", From b26bdcb9b1154ef277eb5686ec0af3d53865e2e7 Mon Sep 17 00:00:00 2001 From: bbm Date: Thu, 26 Mar 2026 15:23:23 -0400 Subject: [PATCH 6/8] update tests --- orsopy/slddb/tests/test_webapi.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/orsopy/slddb/tests/test_webapi.py b/orsopy/slddb/tests/test_webapi.py index aaffc21..5d94118 100644 --- a/orsopy/slddb/tests/test_webapi.py +++ b/orsopy/slddb/tests/test_webapi.py @@ -86,7 +86,7 @@ def test_a_downloaddb(self): if not self.server_available: return # make sure the path of the module is correct and that the database has not been downloaded - self.assertTrue(api.first_access) + self.assertTrue(api.db_needs_update) # self.assertEqual(slddb.__file__, os.path.join(self.path, 'slddb', '__init__.py')) self.assertFalse(os.path.exists(slddb.DB_FILE)) # test of database download @@ -115,26 +115,26 @@ def test_a_downloaddb(self): def test_b_check(self): if not self.server_available: return - api.first_access = True + api.db_needs_update = True if os.path.isfile(slddb.DB_FILE): os.remove(slddb.DB_FILE) api.check() - self.assertFalse(api.first_access) - api.first_access = True + self.assertFalse(api.db_needs_update) + api.db_needs_update = True api.check() - self.assertFalse(api.first_access) + self.assertFalse(api.db_needs_update) api.check() # check the update case api.db.db.close() del api.db - api.first_access = True + api.db_needs_update = True api.max_age = -1 api.check() api.max_age = 1 # check warning if download url doesn't work during update api.db.db.close() del api.db - api.first_access = True + api.db_needs_update = True api.max_age = -1 from orsopy.slddb import dbconfig, webapi From a1530c5e86d8c363092baa7b1fd57514b4b0ae69 Mon Sep 17 00:00:00 2001 From: bbm Date: Thu, 26 Mar 2026 15:25:48 -0400 Subject: [PATCH 7/8] change attribute to 'update_db' since it might be used like slddb.api.update_db = False --- orsopy/slddb/tests/test_webapi.py | 14 +++++++------- orsopy/slddb/webapi.py | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/orsopy/slddb/tests/test_webapi.py b/orsopy/slddb/tests/test_webapi.py index 5d94118..ae6ffd6 100644 --- a/orsopy/slddb/tests/test_webapi.py +++ b/orsopy/slddb/tests/test_webapi.py @@ -86,7 +86,7 @@ def test_a_downloaddb(self): if not self.server_available: return # make sure the path of the module is correct and that the database has not been downloaded - self.assertTrue(api.db_needs_update) + self.assertTrue(api.update_db) # self.assertEqual(slddb.__file__, os.path.join(self.path, 'slddb', '__init__.py')) self.assertFalse(os.path.exists(slddb.DB_FILE)) # test of database download @@ -115,26 +115,26 @@ def test_a_downloaddb(self): def test_b_check(self): if not self.server_available: return - api.db_needs_update = True + api.update_db = True if os.path.isfile(slddb.DB_FILE): os.remove(slddb.DB_FILE) api.check() - self.assertFalse(api.db_needs_update) - api.db_needs_update = True + self.assertFalse(api.update_db) + api.update_db = True api.check() - self.assertFalse(api.db_needs_update) + self.assertFalse(api.update_db) api.check() # check the update case api.db.db.close() del api.db - api.db_needs_update = True + api.update_db = True api.max_age = -1 api.check() api.max_age = 1 # check warning if download url doesn't work during update api.db.db.close() del api.db - api.db_needs_update = True + api.update_db = True api.max_age = -1 from orsopy.slddb import dbconfig, webapi diff --git a/orsopy/slddb/webapi.py b/orsopy/slddb/webapi.py index 65a1898..88e9bc8 100644 --- a/orsopy/slddb/webapi.py +++ b/orsopy/slddb/webapi.py @@ -48,12 +48,12 @@ class SLD_API: db: SLDDB = None def __init__(self, update_db=True): - self.db_needs_update = update_db + self.update_db = update_db self.use_webquery = False # default to using local database, which is updated regularly def check(self): # make sure the local database file is up to date, if not try to download newest version - if self.db_needs_update: + if self.update_db: now = datetime.datetime.now() try: stat = pathlib.Path(DB_FILE).stat() @@ -71,7 +71,7 @@ def check(self): except URLError as err: warnings.warn("Can't download new version of database; " + str(err)) self.db = SLDDB(DB_FILE) # after potential update, make connection with local database - self.db_needs_update = False + self.update_db = False else: return From 430c228e5c3f7d2c595fad2e0050aa2c85f42445 Mon Sep 17 00:00:00 2001 From: bbm Date: Thu, 26 Mar 2026 15:43:27 -0400 Subject: [PATCH 8/8] flake8 --- orsopy/slddb/blender.py | 4 ++-- orsopy/slddb/webapi.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/orsopy/slddb/blender.py b/orsopy/slddb/blender.py index ef1a4c7..357c9b7 100644 --- a/orsopy/slddb/blender.py +++ b/orsopy/slddb/blender.py @@ -65,7 +65,7 @@ def collect_combination(ids, name_dict): elements: list[Material] = [] loaded_ids: dict[str, Material] = {} for id in ids: - if not id in loaded_ids: + if id not in loaded_ids: try: entry = db.search_material(name=ExactString(name_dict[id]))[0] except KeyError: @@ -114,7 +114,7 @@ def collect_blendIDs(formula): ID = int(pre.split('*', 1)[1].strip()) items.append((number, ID)) for number, ID in items: - if not ID in loaded_ids: + if ID not in loaded_ids: entry = db.search_material(ID=ID)[0] m = db.select_material(entry) loaded_ids[ID] = m diff --git a/orsopy/slddb/webapi.py b/orsopy/slddb/webapi.py index 88e9bc8..22e8f11 100644 --- a/orsopy/slddb/webapi.py +++ b/orsopy/slddb/webapi.py @@ -245,6 +245,7 @@ def select_api(args): out = material.export(xray_units=args.get('xray_unit', 'edens')) return out + def search_api(args): """Search the database with the given field values. @@ -288,4 +289,4 @@ def query_api(args): field for field in DB_MATERIALS_FIELDS if field not in DB_MATERIALS_HIDDEN_DATA ] else: - return search_api(args) \ No newline at end of file + return search_api(args)