ose-plugin-bcio 0.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: ose-plugin-bcio
3
+ Version: 0.2.5
4
+ Summary: Plugin for BCIO services and workflows
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: ose-core==0.2.5
8
+ Requires-Dist: ose-plugin-hbcp==0.2.5
9
+ Requires-Dist: GitHub-Flask
10
+ Requires-Dist: Flask-SQLAlchemy==3.1.1
11
+
12
+ # OSE Plugin: BCIO
13
+
14
+ OntoSpreadEd plugin for BCIO (Behaviour Change Intervention Ontology) services and workflows.
15
+
16
+ ## Description
17
+
18
+ This plugin extends OntoSpreadEd with functionality specific to the BCIO ontology project. It provides:
19
+
20
+ - BCIO search release step for automated release pipelines
21
+ - Custom scripts for BCIO vocabulary management:
22
+ - Clean up BCIO vocabulary
23
+ - Import missing external terms
24
+ - Update imports to latest versions
25
+ - Set pre-proposed curation status
26
+ - Integration with BCIO search services
27
+ - Custom UI components for BCIO-specific workflows
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install ose-plugin-bcio
33
+ ```
34
+
35
+ ## Requirements
36
+
37
+ - Python 3.12+
38
+ - ose-core
39
+ - ose-plugin-hbcp
40
+
41
+ ## Configuration
42
+
43
+ The plugin is automatically discovered and loaded when installed. Register it in your release script to use BCIO-specific release steps and scripts.
44
+
45
+ ## License
46
+
47
+ LGPL-3.0-or-later
@@ -0,0 +1,36 @@
1
+ # OSE Plugin: BCIO
2
+
3
+ OntoSpreadEd plugin for BCIO (Behaviour Change Intervention Ontology) services and workflows.
4
+
5
+ ## Description
6
+
7
+ This plugin extends OntoSpreadEd with functionality specific to the BCIO ontology project. It provides:
8
+
9
+ - BCIO search release step for automated release pipelines
10
+ - Custom scripts for BCIO vocabulary management:
11
+ - Clean up BCIO vocabulary
12
+ - Import missing external terms
13
+ - Update imports to latest versions
14
+ - Set pre-proposed curation status
15
+ - Integration with BCIO search services
16
+ - Custom UI components for BCIO-specific workflows
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install ose-plugin-bcio
22
+ ```
23
+
24
+ ## Requirements
25
+
26
+ - Python 3.12+
27
+ - ose-core
28
+ - ose-plugin-hbcp
29
+
30
+ ## Configuration
31
+
32
+ The plugin is automatically discovered and loaded when installed. Register it in your release script to use BCIO-specific release steps and scripts.
33
+
34
+ ## License
35
+
36
+ LGPL-3.0-or-later
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools >= 61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ose-plugin-bcio"
7
+ version = "0.2.5"
8
+ description = "Plugin for BCIO services and workflows"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ dependencies = [
12
+ "ose-core==0.2.5",
13
+ "ose-plugin-hbcp==0.2.5",
14
+ "GitHub-Flask",
15
+ "Flask-SQLAlchemy==3.1.1",
16
+ ]
17
+
18
+ [project.entry-points.'ose.plugins']
19
+ bcio = "ose_plugin_bcio:BCIOPlugin"
20
+
21
+ [tool.uv.sources]
22
+ ose-core = { workspace = true }
23
+ ose-plugin-hbcp = { workspace = true }
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.setuptools.package-data]
29
+ ose_plugin_bcio = ["static/*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,52 @@
1
+ import logging
2
+ from typing import Literal, Dict, Tuple
3
+
4
+ from aiohttp import ClientSession
5
+
6
+ from ose_plugin_hbcp.search_api.APIClient import APIClient
7
+ from ose.model.Term import Term
8
+ from ose.model.TermIdentifier import TermIdentifier
9
+
10
+
11
+ class BCIOSearchClient(APIClient):
12
+ _session: ClientSession
13
+ _logger = logging.getLogger(__name__)
14
+
15
+ _term_link_to_relation_mapping: Dict[
16
+ str, Tuple[TermIdentifier, Literal["single", "multiple", "multiple-per-line"]]
17
+ ] = {
18
+ "synonyms": (TermIdentifier(id="IAO:0000118", label="alternative label"), "multiple"),
19
+ "definition": (TermIdentifier(id="IAO:0000115", label="definition"), "single"),
20
+ "informalDefinition": (TermIdentifier(label="informalDefinition"), "single"),
21
+ "lowerLevelOntology": (TermIdentifier(label="lowerLevelOntology"), "multiple"),
22
+ "curatorNote": (TermIdentifier(id="IAO:0000232", label="curator note"), "single"),
23
+ "curationStatus": (TermIdentifier(id="IAO:0000114", label="has curation status"), "single"),
24
+ "comment": (TermIdentifier(id="rdfs:comment", label="rdfs:comment"), "single"),
25
+ "examples": (TermIdentifier(id="IAO:0000112", label="example of usage"), "multiple-per-line"),
26
+ "fuzzySet": (TermIdentifier(label="fuzzySet"), "single"),
27
+ "fuzzyExplanation": (TermIdentifier(label="fuzzyExplanation"), "single"),
28
+ "crossReferences": (TermIdentifier(label="crossReference"), "multiple"),
29
+ }
30
+
31
+ def convert_to_api_term(self, term: Term, with_references=True) -> Dict:
32
+ data = super().convert_to_api_term(term, with_references)
33
+
34
+ if "lowerLevelOntology" in data and data["lowerLevelOntology"] is not None:
35
+ lower_level_ontologies = [d.lower().strip() for d in data["lowerLevelOntology"]]
36
+ if "upper level" in lower_level_ontologies:
37
+ lower_level_ontologies.remove("upper level")
38
+
39
+ data["lowerLevelOntology"] = lower_level_ontologies
40
+
41
+ return data
42
+
43
+ def terms_equal(self, old: Term, new: Term) -> bool:
44
+ ignore_if_not_exists = [
45
+ "informalDefinition",
46
+ "lowerLevelOntology",
47
+ "fuzzySet",
48
+ "fuzzyExplanation",
49
+ "crossReference",
50
+ ]
51
+
52
+ return self._terms_equal(ignore_if_not_exists, new, old)
@@ -0,0 +1,82 @@
1
+ import asyncio
2
+ from datetime import datetime
3
+ from typing import List
4
+
5
+ import aiohttp
6
+ from flask_github import GitHub
7
+ from flask_sqlalchemy import SQLAlchemy
8
+
9
+ from ose.model.ExcelOntology import ExcelOntology
10
+ from ose.model.ReleaseScript import ReleaseScript
11
+ from ose.model.Result import Result
12
+ from ose.release.ReleaseStep import ReleaseStep
13
+ from ose.release.common import order_sources
14
+ from .BCIOSearchService import BCIOSearchService
15
+ from ose.services.ConfigurationService import ConfigurationService
16
+
17
+
18
+ class BCIOSearchReleaseStep(ReleaseStep):
19
+ _included_files: List[str]
20
+
21
+ @classmethod
22
+ def name(cls) -> str:
23
+ return "BCIO_SEARCH"
24
+
25
+ def __init__(
26
+ self,
27
+ db: SQLAlchemy,
28
+ gh: GitHub,
29
+ release_script: ReleaseScript,
30
+ release_id: int,
31
+ tmp: str,
32
+ config: ConfigurationService,
33
+ *,
34
+ included_files: List[str],
35
+ ):
36
+ super().__init__(db, gh, release_script, release_id, tmp, config)
37
+
38
+ self._included_files = included_files
39
+
40
+ def run(self) -> bool:
41
+ result = Result()
42
+ sources = order_sources(
43
+ dict([(k, f) for k, f in self._release_script.files.items() if k in self._included_files])
44
+ )
45
+
46
+ ontology = ExcelOntology("")
47
+ for s in self._release_script.external.sources:
48
+ xlsx = self._local_name(s.file)
49
+ result += ontology.add_imported_terms(s.file, xlsx)
50
+
51
+ for i, (k, file) in enumerate(sources):
52
+ for s in file.sources:
53
+ if s.type == "classes":
54
+ result += ontology.add_terms_from_excel(s.file, self._local_name(s.file))
55
+ elif s.type == "relations":
56
+ result += ontology.add_relations_from_excel(s.file, self._local_name(s.file))
57
+
58
+ self._raise_if_canceled()
59
+
60
+ ontology.resolve()
61
+ self._raise_if_canceled()
62
+
63
+ ontology.remove_duplicates()
64
+ self._raise_if_canceled()
65
+
66
+ externals = [self._local_name(self._release_script.external.target.file)]
67
+ result += asyncio.run(self.run_service(ontology, externals))
68
+
69
+ self._raise_if_canceled()
70
+
71
+ self._set_release_result(result)
72
+ return result.ok()
73
+
74
+ async def run_service(self, ontology: ExcelOntology, externals: List[str]) -> Result[tuple]:
75
+ async with aiohttp.ClientSession() as session:
76
+ service = BCIOSearchService(self._config, session)
77
+ return await service.update_api(
78
+ ontology,
79
+ externals,
80
+ f"{datetime.utcnow().strftime('%B %Y')} Release",
81
+ lambda step, total, msg: self._update_progress(position=(step, total), current_item=msg),
82
+ )
@@ -0,0 +1,31 @@
1
+ import logging
2
+ import os
3
+
4
+ import aiohttp
5
+
6
+ from ose_plugin_hbcp.search_api.APIService import APIService
7
+ from .BCIOSearchClient import BCIOSearchClient
8
+ from ose.services.ConfigurationService import ConfigurationService
9
+
10
+ PROP_BCIO_SEARCH_API_PATH = "BCIO_SEARCH_API_PATH"
11
+ PROP_BCIO_SEARCH_API_AUTH_TOKEN = "BCIO_SEARCH_API_AUTH_TOKEN"
12
+
13
+
14
+ class BCIOSearchService(APIService[BCIOSearchClient]):
15
+ _logger = logging.getLogger(__name__)
16
+
17
+ def __init__(self, config: ConfigurationService, session: aiohttp.ClientSession):
18
+ path = config.app_config.get(PROP_BCIO_SEARCH_API_PATH, os.environ.get(PROP_BCIO_SEARCH_API_PATH))
19
+ auth_token = config.app_config.get(
20
+ PROP_BCIO_SEARCH_API_AUTH_TOKEN, os.environ.get(PROP_BCIO_SEARCH_API_AUTH_TOKEN, None)
21
+ )
22
+
23
+ api_client = BCIOSearchClient(
24
+ path, session, auth_token, config.app_config.get("ENVIRONMENT", "debug") != "production"
25
+ )
26
+
27
+ super().__init__(config, api_client)
28
+
29
+ @property
30
+ def repository_name(self):
31
+ return "BCIO"
@@ -0,0 +1,28 @@
1
+ from ose.model.Plugin import Plugin, PluginComponent
2
+ from .BCIOSearchReleaseStep import BCIOSearchReleaseStep
3
+ from .scripts.cleanup_bcio_vocab import CleanUpBCIOVocabScript
4
+ from .scripts.import_missing_externals import ImportMissingExternalsScript
5
+ from .scripts.update_imports_to_latest_versions import UpdateImportsToLatestVersionsScript
6
+ from .scripts.set_pre_proposed_curation_status import SetPreProposedCurationStatusScript
7
+
8
+
9
+ BCIOPlugin = Plugin(
10
+ id="org.bssofoundry.bcio",
11
+ name="BCIO Plugin",
12
+ version="0.1.0",
13
+ description="Plugin for BCIO services and workflows.",
14
+ contents=[
15
+ BCIOSearchReleaseStep,
16
+ CleanUpBCIOVocabScript,
17
+ ImportMissingExternalsScript,
18
+ UpdateImportsToLatestVersionsScript,
19
+ SetPreProposedCurationStatusScript,
20
+ ],
21
+ components=[
22
+ PluginComponent(
23
+ step_name="BCIO_SEARCH",
24
+ component_name="BCIOSearch",
25
+ ),
26
+ ],
27
+ js_module="ose-plugin-bcio.js",
28
+ )
@@ -0,0 +1,91 @@
1
+ import base64
2
+ import io
3
+ import json
4
+
5
+ import aiohttp
6
+ from flask_github import GitHub
7
+ from injector import inject
8
+
9
+ from ose.model.ExcelOntology import ExcelOntology
10
+ from ose.model.Script import Script
11
+ from ose.model.TermIdentifier import TermIdentifier
12
+ from ..BCIOSearchService import BCIOSearchService
13
+ from ose.services.ConfigurationService import ConfigurationService
14
+ from ose.utils import get_spreadsheets, get_spreadsheet
15
+
16
+
17
+ class CleanUpBCIOVocabScript(Script):
18
+ @property
19
+ def id(self) -> str:
20
+ return "cleanup-bcio-vocab"
21
+
22
+ @property
23
+ def title(self) -> str:
24
+ return "Cleanup terms on BCIO Vocab"
25
+
26
+ @inject
27
+ def __init__(self, gh: GitHub, config: ConfigurationService) -> None:
28
+ super().__init__()
29
+ self.gh = gh
30
+ self.config = config
31
+
32
+ async def run(self) -> str:
33
+ repo = self.config.get("BCIO")
34
+
35
+ assert repo is not None, "BCIO repository configuration not found."
36
+
37
+ # get all BCIO Vocab terms
38
+ async with aiohttp.ClientSession() as session:
39
+ service = BCIOSearchService(self.config, session)
40
+ bcio_terms = await service.get_all_terms()
41
+
42
+ bcio_vocab_by_id = dict([(t.id.strip(), t) for t in bcio_terms if t.id is not None])
43
+
44
+ # get all BCIO excel terms
45
+ branch = repo.main_branch
46
+ active_sheets = repo.indexed_files
47
+ regex = "|".join(f"({r})" for r in active_sheets)
48
+
49
+ excel_files = get_spreadsheets(self.gh, repo.full_name, branch, include_pattern=regex)
50
+
51
+ for file in excel_files:
52
+ _, data, _ = get_spreadsheet(self.gh, repo.full_name, file)
53
+
54
+ for entity in data:
55
+ term_id = entity.get("ID", "").strip()
56
+ if term_id in bcio_vocab_by_id:
57
+ del bcio_vocab_by_id[term_id]
58
+
59
+ exclude = set()
60
+ for k, v in bcio_vocab_by_id.items():
61
+ # Population is expanded
62
+ if v.get_relation_value(TermIdentifier(id=None, label="lowerLevelOntology")) == "population":
63
+ exclude.add(k)
64
+
65
+ if v.curation_status() == "external":
66
+ exclude.add(k)
67
+
68
+ for k in exclude:
69
+ del bcio_vocab_by_id[k]
70
+
71
+ with open("ids.json", "w") as f:
72
+ json.dump(bcio_vocab_by_id, f)
73
+
74
+ excel_ontology = ExcelOntology("tmp://tmp")
75
+ for t in bcio_vocab_by_id.values():
76
+ excel_ontology.add_term(t)
77
+
78
+ excel = excel_ontology.to_excel()
79
+ excel.save("to_be_deleted.xlsx")
80
+
81
+ stream = io.BytesIO()
82
+ excel.save(stream)
83
+
84
+ # stream to base64
85
+ b64content = base64.b64encode(stream.getvalue()).decode()
86
+
87
+ return (
88
+ f"The entities in this excel sheet have been identified to be deleted. "
89
+ f'<a download="to_be_deleted.xlsx" href="data:application/vnd.ms-excel;base64,{b64content}>'
90
+ f"to_be_deleted.xlsx</a>"
91
+ )
@@ -0,0 +1,118 @@
1
+ from datetime import datetime
2
+ from typing import Annotated, Dict
3
+
4
+ from flask_github import GitHub
5
+ from injector import inject
6
+ import pyhornedowl
7
+
8
+ from ose.model.Script import Script
9
+ import ose.utils.github as github
10
+ from ose import constants
11
+ from ose.model.ExcelOntology import ExcelOntology, OntologyImport
12
+ from ose.model.Term import Term
13
+ from ose.model.TermIdentifier import TermIdentifier
14
+ # from ose.routes.api.external import update_imports
15
+ from ose.services.ConfigurationService import ConfigurationService
16
+ from ose.utils import get_spreadsheets, lower
17
+
18
+
19
+ class ImportMissingExternalsScript(Script):
20
+ @property
21
+ def id(self) -> str:
22
+ return "import-missing-externals"
23
+
24
+ @property
25
+ def title(self) -> str:
26
+ return "Import Missing Externals"
27
+
28
+ @inject
29
+ def __init__(self, gh: GitHub, config: ConfigurationService) -> None:
30
+ super().__init__()
31
+ self.gh = gh
32
+ self.config = config
33
+
34
+ def run(self, repo: Annotated[str, "Repository name"]) -> str:
35
+ repository = self.config.get(repo)
36
+
37
+ assert repository is not None, f"Repository configuration for '{repo}' not found."
38
+
39
+ full_repo = repository.full_name
40
+ branch = repository.main_branch
41
+ active_sheets = repository.indexed_files
42
+ regex = "|".join(f"({r})" for r in active_sheets)
43
+
44
+ externals_owl = "addicto_external.owl"
45
+ externals_content = github.get_file(self.gh, full_repo, externals_owl).decode()
46
+
47
+ external_ontology = ExcelOntology(externals_owl)
48
+ ontology = pyhornedowl.open_ontology(externals_content)
49
+ for p, d in repository.prefixes.items():
50
+ ontology.prefix_mapping.add_prefix(p, d)
51
+
52
+ for c in ontology.get_classes():
53
+ id = ontology.get_id_for_iri(c)
54
+ labels = ontology.get_annotations(c, constants.RDFS_LABEL)
55
+
56
+ if id is not None:
57
+ for label in labels:
58
+ external_ontology.add_term(
59
+ Term(
60
+ id=id,
61
+ label=label,
62
+ origin=("<external>", -1),
63
+ relations=[],
64
+ sub_class_of=[],
65
+ equivalent_to=[],
66
+ disjoint_with=[],
67
+ )
68
+ )
69
+
70
+ excel_files = get_spreadsheets(self.gh, full_repo, branch, include_pattern=regex)
71
+
72
+ missing_imports: Dict[str, OntologyImport] = {}
73
+
74
+ for file in excel_files:
75
+ content = github.get_file(self.gh, full_repo, file)
76
+ o = ExcelOntology(file)
77
+ o.add_terms_from_excel(file, content)
78
+
79
+ for t in o._terms:
80
+ if (
81
+ t.id is not None
82
+ and lower(t.curation_status()) == "external"
83
+ and external_ontology.term_by_id(t.id) is None
84
+ ):
85
+ pref = t.id.split(":")[0]
86
+ imp = missing_imports.setdefault(
87
+ pref,
88
+ OntologyImport(
89
+ id=pref,
90
+ iri=f"http://purl.obolibrary.org/obo/{pref.lower()}.owl",
91
+ root_id=TermIdentifier("BFO:0000001", "entity"),
92
+ intermediates="all",
93
+ prefixes=[],
94
+ imported_terms=[],
95
+ ),
96
+ )
97
+
98
+ imp.imported_terms.append(t.identifier())
99
+
100
+ change_branch = f"script/import-missing-{datetime.utcnow().strftime('%Y-%m-%d_%H-%M-%S')}"
101
+ github.create_branch(self.gh, full_repo, change_branch, branch)
102
+
103
+
104
+ # update_imports(repo, full_repo, self.gh, list(missing_imports.values()), change_branch)
105
+
106
+ body = "Imported the terms:\n"
107
+ body += "\n\n".join(
108
+ f"{m.id}: \n" + "\n".join(f" - {t.label} [{t.id}]" for t in m.imported_terms)
109
+ for m in missing_imports.values()
110
+ )
111
+
112
+ pr = github.create_pr(self.gh, full_repo, "Import missing external terms", body, change_branch, branch)
113
+ github.merge_pr(self.gh, full_repo, pr)
114
+
115
+ return "<p>Imported the terms:</p>\n" + "\n".join(
116
+ f"<p>{m.id}: \n<ul>" + "\n".join(f"<li>{t.label} [{t.id}]</li>" for t in m.imported_terms) + "</ul></p>"
117
+ for m in missing_imports.values()
118
+ )
@@ -0,0 +1,124 @@
1
+ from io import BytesIO
2
+ from typing import Annotated, Union
3
+
4
+ from injector import inject
5
+ import openpyxl
6
+ from flask_github import GitHub
7
+ from openpyxl.styles import PatternFill
8
+ import pyhornedowl
9
+
10
+ from ose.model.Script import Script
11
+ import ose.utils.github as github
12
+ from ose import constants
13
+ from ose.model.ExcelOntology import ExcelOntology
14
+ from ose.model.Term import UnresolvedTerm, Term
15
+ from ose.services.ConfigurationService import ConfigurationService
16
+ from ose.utils import get_spreadsheets, str_empty
17
+
18
+
19
+ def is_incomplete(term: Union[UnresolvedTerm, Term]):
20
+ return str_empty(term.label) or len(term.sub_class_of) <= 0 or str_empty(term.definition())
21
+
22
+
23
+ class SetPreProposedCurationStatusScript(Script):
24
+ @property
25
+ def id(self) -> str:
26
+ return "set-pre-proposed-curation-status"
27
+
28
+ @property
29
+ def title(self) -> str:
30
+ return "Set Pre-proposed Curation Status"
31
+
32
+ @inject
33
+ def __init__(self, gh: GitHub, config: ConfigurationService) -> None:
34
+ super().__init__()
35
+ self.gh = gh
36
+ self.config = config
37
+
38
+ def run(self, repo: Annotated[str, "Repository name"]) -> str:
39
+ repository = self.config.get(repo)
40
+
41
+ assert repository is not None, f"Repository configuration for '{repo}' not found."
42
+
43
+ full_repo = repository.full_name
44
+ branch = repository.main_branch
45
+ active_sheets = repository.indexed_files
46
+ regex = "|".join(f"({r})" for r in active_sheets)
47
+
48
+ externals_owl = "addicto_external.owl"
49
+ externals_content = github.get_file(self.gh, full_repo, externals_owl).decode()
50
+
51
+ external_ontology = ExcelOntology(externals_owl)
52
+ ontology = pyhornedowl.open_ontology(externals_content, "rdf")
53
+ for [p, d] in repository.prefixes:
54
+ ontology.prefix_mapping.add_prefix(p, d)
55
+
56
+ for c in ontology.get_classes():
57
+ id = ontology.get_id_for_iri(c)
58
+ labels = ontology.get_annotations(c, constants.RDFS_LABEL)
59
+
60
+ if id is not None:
61
+ for label in labels:
62
+ external_ontology.add_term(Term(
63
+ id=id,
64
+ label=label,
65
+ origin=("<external>", -1),
66
+ relations=[],
67
+ sub_class_of=[],
68
+ equivalent_to=[],
69
+ disjoint_with=[]
70
+ ))
71
+
72
+ excel_files = get_spreadsheets(self.gh, full_repo, branch, include_pattern=regex)
73
+
74
+ changed_files = 0
75
+
76
+ for file in excel_files:
77
+ has_changed = False
78
+ content = github.get_file(self.gh, full_repo, file)
79
+ o = ExcelOntology(file)
80
+ o.import_other_excel_ontology(external_ontology)
81
+ o.add_terms_from_excel(file, content)
82
+
83
+ wb = openpyxl.load_workbook(BytesIO(content))
84
+ sheet = wb.active
85
+
86
+ assert sheet is not None, f"Could not load active sheet from {file}"
87
+
88
+ header = [cell.value for cell in sheet[1] if cell.value]
89
+
90
+ if "Label" not in header or "Curation status" not in header:
91
+ continue
92
+
93
+ c_label = header.index("Label")
94
+ c_curation_status = header.index("Curation status")
95
+
96
+ o.resolve()
97
+
98
+ for row in sheet:
99
+ label = row[c_label].value
100
+ label = label.strip() if isinstance(label, str) else None
101
+ term = o._term_by_label(label) if label is not None else None
102
+
103
+ if term is not None and (str_empty(term.id) or term.id.startswith(repo.upper())):
104
+ incomplete = is_incomplete(term)
105
+ incomplete = incomplete or all(
106
+ p.is_unresolved() or
107
+ o._term_by_id(p.id) is not None and o._term_by_id(p.id).curation_status() == 'Pre-proposed'
108
+ for p in term.sub_class_of
109
+ )
110
+ if incomplete and row[c_curation_status].value != "Pre-proposed":
111
+ row[c_curation_status].value = "Pre-proposed"
112
+ for c in row:
113
+ c.fill = PatternFill(fgColor="ebfad0", fill_type="solid")
114
+
115
+ changed_files += 1
116
+ has_changed = True
117
+
118
+ if has_changed:
119
+ spreadsheet_stream = BytesIO()
120
+ wb.save(spreadsheet_stream)
121
+ msg = f"Updating {file.split('/')[-1]}\n\nSet terms to Pre-proposed"
122
+ github.save_file(self.gh, full_repo, file, spreadsheet_stream.getvalue(), msg, branch)
123
+
124
+ return f"Updated {changed_files} terms in {len(excel_files)} spreadsheets."
@@ -0,0 +1,107 @@
1
+ import json
2
+ from io import BytesIO
3
+ from typing import Annotated, List
4
+
5
+ from injector import inject
6
+ import openpyxl
7
+ import pyhornedowl
8
+ import requests
9
+ from flask_github import GitHub
10
+
11
+ from ose.model.Script import Script
12
+ import ose.utils.github as github
13
+ from ose.model.ReleaseScript import ReleaseScript
14
+ from ose.model.Result import Result
15
+ from ose.services.ConfigurationService import ConfigurationService
16
+ from ose.utils import str_space_eq, lower
17
+
18
+ class UpdateImportsToLatestVersionsScript(Script):
19
+ @property
20
+ def id(self) -> str:
21
+ return "update-imports-to-latest-versions"
22
+
23
+ @property
24
+ def title(self) -> str:
25
+ return "Update Imports to Latest Versions"
26
+
27
+ @inject
28
+ def __init__(self, gh: GitHub, config: ConfigurationService) -> None:
29
+ self.gh = gh
30
+ self.config = config
31
+
32
+ def run(self, repo: Annotated[str, "Repository name"]) -> str:
33
+ result = Result([])
34
+ repository = self.config.get(repo)
35
+
36
+ assert repository is not None, f"Repository configuration for '{repo}' not found."
37
+
38
+ release_script_json = self.config.get_file(repository, repository.release_script_path)
39
+
40
+ assert release_script_json is not None, f"Release script not found at '{repository.release_script_path}'"
41
+
42
+ release_script = ReleaseScript.from_json(json.loads(release_script_json))
43
+
44
+ for source in release_script.external.sources:
45
+ file = self.config.get_file_raw(repository, source.file)
46
+ assert file is not None, f"Could not load file '{source.file}' from repository '{repository.full_name}'"
47
+ file = BytesIO(file)
48
+
49
+ wb = openpyxl.load_workbook(file)
50
+ sheet = wb.active
51
+
52
+ assert sheet is not None, f"Could not load active sheet from '{source.file}'"
53
+
54
+ rows = sheet.rows
55
+
56
+ header = next(rows)
57
+ iri_index = next((i for i, h in enumerate(header) if
58
+ str_space_eq(lower(str(h.value)), "purl") or str_space_eq(lower(str(h.value)), "iri")), None)
59
+ version_index = next((i for i, h in enumerate(header) if str_space_eq(lower(str(h.value)), "version")), None)
60
+ id_index = next((i for i, h in enumerate(header) if str_space_eq(lower(str(h.value)), "ontology id")), None)
61
+
62
+ if None in [iri_index, version_index, id_index]:
63
+ continue
64
+
65
+ changes: List[str] = []
66
+ for row in rows:
67
+ id = row[id_index].value
68
+ iri = row[iri_index].value
69
+
70
+ if id in ["GAZ", "OPMI"]: # GAZ is weird and crashes HornedOwl
71
+ continue
72
+
73
+ try:
74
+ response = requests.get(iri)
75
+ content = response.text
76
+
77
+ serialisation = iri[iri.rfind(".") + 1:]
78
+ onto = pyhornedowl.open_ontology_from_string(content, serialisation)
79
+
80
+ version_iri = onto.get_version_iri()
81
+
82
+ old_version = row[version_index].value
83
+ if version_iri is None:
84
+ result.warning(type="no-version-iri",
85
+ msg=f"Ontology '{id}' has no version IRI")
86
+ else:
87
+ version_iri = str(version_iri)
88
+ if not str_space_eq(old_version, version_iri):
89
+ row[version_index].value = version_iri
90
+
91
+ changes.append(f"Updated '{id}' from {old_version} to {version_iri}")
92
+
93
+ except Exception as e:
94
+ result.error(type="load-ontology",
95
+ msg=f"Failed to load external ontology '{id}' from '{iri}'",
96
+ e=e)
97
+
98
+ if len(changes) > 0:
99
+ spreadsheet_stream = BytesIO()
100
+ wb.save(spreadsheet_stream)
101
+
102
+ github.save_file(self.gh, repository.full_name, source.file, spreadsheet_stream.getvalue(), "\n".join(changes),
103
+ repository.main_branch)
104
+
105
+ result.value += changes
106
+
107
+ return "\n".join(result.value)
@@ -0,0 +1,39 @@
1
+ import { defineComponent as o, createElementBlock as a, openBlock as e, Fragment as i, createElementVNode as s, createBlock as d, createCommentVNode as c, renderList as h, toDisplayString as n, unref as u, withCtx as m, createTextVNode as p } from "vue";
2
+ import { ProgressIndicator as g } from "@ose/js-core";
3
+ const y = { class: "alert alert-danger" }, k = { key: 1 }, f = { key: 2 }, b = /* @__PURE__ */ o({
4
+ __name: "BCIOSearch",
5
+ props: {
6
+ data: {},
7
+ release: {},
8
+ selectedSubStep: {}
9
+ },
10
+ emits: ["release-control"],
11
+ setup(t) {
12
+ return (S, r) => (e(), a(i, null, [
13
+ r[1] || (r[1] = s("h3", null, "Publishing the release", -1)),
14
+ t.release.state === "waiting-for-user" && t.data?.errors?.length > 0 ? (e(!0), a(i, { key: 0 }, h(t.data.errors, (l) => (e(), a("div", y, [
15
+ l.details && l?.response?.["hydra:description"] ? (e(), a(i, { key: 0 }, [
16
+ s("h4", null, n(l.response["hydra:title"]), 1),
17
+ s("p", null, n(l.details), 1),
18
+ s("p", null, n(l.response["hydra:description"]), 1)
19
+ ], 64)) : (e(), a("pre", k, n(JSON.stringify(l, void 0, 2)), 1))
20
+ ]))), 256)) : (e(), d(u(g), {
21
+ key: 1,
22
+ details: t.data,
23
+ release: t.release
24
+ }, {
25
+ default: m(() => [...r[0] || (r[0] = [
26
+ s("p", null, [
27
+ p(" The ontologies are being published to BCIOSearch. This will take a while."),
28
+ s("br")
29
+ ], -1)
30
+ ])]),
31
+ _: 1
32
+ }, 8, ["details", "release"])),
33
+ t.release.state === "completed" ? (e(), a("p", f, " The ontologies were published to BCIOSearch. ")) : c("", !0)
34
+ ], 64));
35
+ }
36
+ });
37
+ export {
38
+ b as BCIOSearch
39
+ };
@@ -0,0 +1 @@
1
+ (function(t,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue"),require("@ose/js-core")):typeof define=="function"&&define.amd?define(["exports","vue","@ose/js-core"],e):(t=typeof globalThis<"u"?globalThis:t||self,e(t.ose_plugin_bcio={},t.Vue,t.OseJsCore))})(this,(function(t,e,r){"use strict";const s={class:"alert alert-danger"},i={key:1},a={key:2},c=e.defineComponent({__name:"BCIOSearch",props:{data:{},release:{},selectedSubStep:{}},emits:["release-control"],setup(n){return(d,l)=>(e.openBlock(),e.createElementBlock(e.Fragment,null,[l[1]||(l[1]=e.createElementVNode("h3",null,"Publishing the release",-1)),n.release.state==="waiting-for-user"&&n.data?.errors?.length>0?(e.openBlock(!0),e.createElementBlock(e.Fragment,{key:0},e.renderList(n.data.errors,o=>(e.openBlock(),e.createElementBlock("div",s,[o.details&&o?.response?.["hydra:description"]?(e.openBlock(),e.createElementBlock(e.Fragment,{key:0},[e.createElementVNode("h4",null,e.toDisplayString(o.response["hydra:title"]),1),e.createElementVNode("p",null,e.toDisplayString(o.details),1),e.createElementVNode("p",null,e.toDisplayString(o.response["hydra:description"]),1)],64)):(e.openBlock(),e.createElementBlock("pre",i,e.toDisplayString(JSON.stringify(o,void 0,2)),1))]))),256)):(e.openBlock(),e.createBlock(e.unref(r.ProgressIndicator),{key:1,details:n.data,release:n.release},{default:e.withCtx(()=>[...l[0]||(l[0]=[e.createElementVNode("p",null,[e.createTextVNode(" The ontologies are being published to BCIOSearch. This will take a while."),e.createElementVNode("br")],-1)])]),_:1},8,["details","release"])),n.release.state==="completed"?(e.openBlock(),e.createElementBlock("p",a," The ontologies were published to BCIOSearch. ")):e.createCommentVNode("",!0)],64))}});t.BCIOSearch=c,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})}));
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: ose-plugin-bcio
3
+ Version: 0.2.5
4
+ Summary: Plugin for BCIO services and workflows
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: ose-core==0.2.5
8
+ Requires-Dist: ose-plugin-hbcp==0.2.5
9
+ Requires-Dist: GitHub-Flask
10
+ Requires-Dist: Flask-SQLAlchemy==3.1.1
11
+
12
+ # OSE Plugin: BCIO
13
+
14
+ OntoSpreadEd plugin for BCIO (Behaviour Change Intervention Ontology) services and workflows.
15
+
16
+ ## Description
17
+
18
+ This plugin extends OntoSpreadEd with functionality specific to the BCIO ontology project. It provides:
19
+
20
+ - BCIO search release step for automated release pipelines
21
+ - Custom scripts for BCIO vocabulary management:
22
+ - Clean up BCIO vocabulary
23
+ - Import missing external terms
24
+ - Update imports to latest versions
25
+ - Set pre-proposed curation status
26
+ - Integration with BCIO search services
27
+ - Custom UI components for BCIO-specific workflows
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install ose-plugin-bcio
33
+ ```
34
+
35
+ ## Requirements
36
+
37
+ - Python 3.12+
38
+ - ose-core
39
+ - ose-plugin-hbcp
40
+
41
+ ## Configuration
42
+
43
+ The plugin is automatically discovered and loaded when installed. Register it in your release script to use BCIO-specific release steps and scripts.
44
+
45
+ ## License
46
+
47
+ LGPL-3.0-or-later
@@ -0,0 +1,18 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/ose_plugin_bcio/BCIOSearchClient.py
4
+ src/ose_plugin_bcio/BCIOSearchReleaseStep.py
5
+ src/ose_plugin_bcio/BCIOSearchService.py
6
+ src/ose_plugin_bcio/__init__.py
7
+ src/ose_plugin_bcio.egg-info/PKG-INFO
8
+ src/ose_plugin_bcio.egg-info/SOURCES.txt
9
+ src/ose_plugin_bcio.egg-info/dependency_links.txt
10
+ src/ose_plugin_bcio.egg-info/entry_points.txt
11
+ src/ose_plugin_bcio.egg-info/requires.txt
12
+ src/ose_plugin_bcio.egg-info/top_level.txt
13
+ src/ose_plugin_bcio/scripts/cleanup_bcio_vocab.py
14
+ src/ose_plugin_bcio/scripts/import_missing_externals.py
15
+ src/ose_plugin_bcio/scripts/set_pre_proposed_curation_status.py
16
+ src/ose_plugin_bcio/scripts/update_imports_to_latest_versions.py
17
+ src/ose_plugin_bcio/static/ose-plugin-bcio.js
18
+ src/ose_plugin_bcio/static/ose-plugin-bcio.umd.cjs
@@ -0,0 +1,2 @@
1
+ [ose.plugins]
2
+ bcio = ose_plugin_bcio:BCIOPlugin
@@ -0,0 +1,4 @@
1
+ ose-core==0.2.5
2
+ ose-plugin-hbcp==0.2.5
3
+ GitHub-Flask
4
+ Flask-SQLAlchemy==3.1.1
@@ -0,0 +1,2 @@
1
+ ose-plugin-bcio-js
2
+ ose_plugin_bcio