diff --git a/.github/workflows/validate_and_release.yml b/.github/workflows/validate_and_release.yml index 30657fb88..c1411e6c7 100644 --- a/.github/workflows/validate_and_release.yml +++ b/.github/workflows/validate_and_release.yml @@ -46,12 +46,32 @@ jobs: uses: actions/setup-python@v5 with: python-version: 3.12 - + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools + pip install linkml astor pre-commit + pip install git+https://github.com/ReproNim/reproschema-py.git + - name: Generate pydantic using linml and fixing it with reproschema specific script + run: | + gen-pydantic --pydantic-version 2 linkml-schema/reproschema.yaml > reproschema_model_autogen.py + python scripts/fix_pydantic.py reproschema_model_autogen.py reproschema_model.py + pre-commit run --files reproschema_model.py || true + - name: Generate jsonld format using linkml + run: | + gen-jsonld --context contexts/reproschema linkml-schema/reproschema.yaml > reproschema.jsonld + - name: Generate n-triples and turtle formats using reproschema + run: | + reproschema convert --format n-triples reproschema.jsonld > reproschema.nt + reproschema convert --format turtle reproschema.jsonld > reproschema.ttl - name: Make a release run: | echo "Making a release ${{ inputs.version }}" mkdir releases/${{ inputs.version }} - cp contexts/reproschema releases/${{ inputs.version }}/base + cp contexts/reproschema releases/${{ inputs.version }}/reproschema + cp reproschema_model.py releases/${{ inputs.version }}/reproschema_model.py + cp reproschema.jsonld releases/${{ inputs.version }}/reproschema.jsonld + cp reproschema.nt releases/${{ inputs.version }}/reproschema.nt + cp reproschema.ttl releases/${{ inputs.version }}/reproschema.ttl # python scripts/makeRelease.py ${{ inputs.version }} - name: Open pull requests to add files diff --git a/linkml-schema/reproschema.yaml b/linkml-schema/reproschema.yaml index 8afcaf3fd..912d981c8 100644 --- a/linkml-schema/reproschema.yaml +++ b/linkml-schema/reproschema.yaml @@ -55,7 +55,7 @@ slots: slot_uri: schema:associatedMedia audio: title: audio - description: TODO + description: An audio object. slot_uri: schema:audio any_of: - range: uri @@ -101,12 +101,8 @@ slots: contentUrl: slot_uri: schema:contentUrl range: uriorcurie - creator: - slot_uri: schema:creator - range: Person cronTable: title: cronTable - description: TODO not described in reproschema slot_uri: reproschema:cronTable datumType: title: datumType @@ -264,7 +260,7 @@ slots: range: OverrideProperty preamble: title: Preamble - description: The preamble for an assessment + description: The preamble for an assessment. slot_uri: reproschema:preamble multivalued: true range: langString @@ -326,7 +322,7 @@ slots: range: datetime slot_uri: prov:startedAtTime subject_id: - slot_uri: nidm:subject_id #TODO check this @type:rdf:Property + slot_uri: nidm:subject_id range: string ui: title: UI @@ -502,7 +498,7 @@ classes: class_uri: rdf:langString MediaObject: title: Media Object - description: Add description #TODO + description: A media object, such as an image, video, audio, or text object embedded in a web page or a downloadable dataset. is_a: Thing class_uri: schema:MediaObject slots: @@ -542,7 +538,7 @@ classes: - id - subject_id class_uri: reproschema:Participant - Protocol: # TODO multiple types + Protocol: title: Protocol description: A representation of a study which comprises one or more assessments. is_a: Thing @@ -555,6 +551,7 @@ classes: - description - landingPage - messages + - preamble - prefLabel - schemaVersion - ui @@ -596,7 +593,7 @@ classes: - unitOptions - valueType class_uri: reproschema:ResponseOption - SoftwareAgent: # TODO multiple types + SoftwareAgent: title: Software Agent description: Captures information about some action that took place. It also links to information @@ -616,9 +613,8 @@ classes: - category class_uri: schema:Thing UI: - title: todo - description: - - todo + title: UI properties + description: A group of properties related to UI. slots: - order - addProperties diff --git a/scripts/fix_pydantic.py b/scripts/fix_pydantic.py new file mode 100644 index 000000000..b82f5d295 --- /dev/null +++ b/scripts/fix_pydantic.py @@ -0,0 +1,102 @@ +""" Using ast transformer to fix issues with automatic pydantic generation""" + +import ast +import sys + +import astor + + +class ClassRemover(ast.NodeTransformer): + def __init__(self, class_name): + self.class_name = class_name + + def visit_ClassDef(self, node): + # Remove the class if its name matches the class_to_remove + if node.name == self.class_name: + return None + return node + + def visit_Expr(self, node): + # Check if the node is a call expression + if isinstance(node.value, ast.Call): + # Check if the call expression is an attribute (method call) + if isinstance(node.value.func, ast.Attribute): + # Check if the method call matches the specified class + if ( + isinstance(node.value.func.value, ast.Name) + and node.value.func.value.id == self.class_name + ): + return None # Remove this node + return self.generic_visit(node) + + +class TypeReplacer(ast.NodeTransformer): + def __init__(self, old_type, new_type): + self.old_type = old_type + self.new_type = new_type + + def visit_FunctionDef(self, node): + # Check all arguments in the function definition + for arg in node.args.args: + if arg.annotation: + arg.annotation = self.visit(arg.annotation) + return self.generic_visit(node) + + def visit_AsyncFunctionDef(self, node): + # Handle async function definitions similarly + for arg in node.args.args: + if arg.annotation: + arg.annotation = self.visit(arg.annotation) + return self.generic_visit(node) + + def visit_Name(self, node): + # Replace the old type with the new type + if node.id == self.old_type: + node.id = self.new_type + return node + + def visit_Subscript(self, node): + # Handle Union, Optional, and other subscripted types + node.value = self.visit(node.value) + node.slice = self.visit(node.slice) + return node + + def visit_Index(self, node): + # Handle the index part of subscripted types + node.value = self.visit(node.value) + return node + + def visit_Tuple(self, node): + # Handle tuples in type annotations + node.elts = [self.visit(elt) for elt in node.elts] + return node + + +def edit_pydantic(input_file, output_file): + + with open(input_file, "r") as file: + tree = ast.parse(file.read()) + + transformer_class = ClassRemover(class_name="LangString") + tree_modclass = transformer_class.visit(tree) + + transformer_tp = TypeReplacer( + old_type="LangString", new_type="Dict[str, str]" + ) + tree_modclass_modtype = transformer_tp.visit(tree_modclass) + + with open(output_file, "w") as file: + file.write(astor.to_source(tree_modclass_modtype)) + + +if __name__ == "__main__": + input_file = sys.argv[1] + if len(sys.argv) < 3: + output_file = input_file + else: + output_file = sys.argv[2] + print( + f"Fixing automatically generated pydantic file {input_file} " + f"and saving to {output_file}" + ) + edit_pydantic(input_file, output_file)