diff --git a/conftest.py b/conftest.py
index 8fae185..6ea533e 100644
--- a/conftest.py
+++ b/conftest.py
@@ -1 +1,20 @@
# placing this in the root allows the tests in the 'tests' directory to import the packages in the root, e.g. 'dags'
+import pathlib
+
+import pytest
+import rdflib
+
+rdf_file = pathlib.Path(__file__).parent / "tests" / "fixtures" / "bf.ttl"
+
+
+@pytest.fixture
+def test_graph():
+ graph = rdflib.Graph()
+ for ns in [
+ ("bf", "http://id.loc.gov/ontologies/bibframe/"),
+ ("bflc", "http://id.loc.gov/ontologies/bflc/"),
+ ("sinopia", "http://sinopia.io/vocabulary/"),
+ ]:
+ graph.namespace_manager.bind(ns[0], ns[1])
+ graph.parse(rdf_file, format="turtle")
+ return graph
diff --git a/ils_middleware/tasks/folio/mappings/bf_instance.py b/ils_middleware/tasks/folio/mappings/bf_instance.py
new file mode 100644
index 0000000..8f524f4
--- /dev/null
+++ b/ils_middleware/tasks/folio/mappings/bf_instance.py
@@ -0,0 +1,167 @@
+date_of_publication = """PREFIX bf:
+PREFIX rdf:
+PREFIX rdfs:
+
+SELECT ?date
+WHERE {{
+ <{bf_instance}> a bf:Instance .
+ <{bf_instance}> bf:provisionActivity ?activity .
+ ?activity a bf:Publication .
+ ?activity bf:date ?date .
+}}
+"""
+
+identifier = """PREFIX bf:
+PREFIX rdf:
+PREFIX rdfs:
+
+SELECT ?identifier
+WHERE {{
+ <{bf_instance}> a bf:Instance .
+ <{bf_instance}> bf:identifiedBy ?ident_bnode .
+ ?ident_bnode a {bf_class} .
+ ?ident_bnode rdf:value ?identifier .
+}}
+"""
+
+instance_format_category = """PREFIX bf:
+PREFIX rdf:
+PREFIX rdfs:
+
+SELECT ?format_category
+WHERE {{
+ <{bf_instance}> a bf:Instance .
+ <{bf_instance}> bf:media ?format_category .
+}}
+"""
+
+instance_format_term = """PREFIX bf:
+PREFIX rdf:
+PREFIX rdfs:
+
+SELECT ?format_term
+WHERE {{
+ <{bf_instance}> a bf:Instance .
+ <{bf_instance}> bf:carrier ?format_term .
+}}
+"""
+
+local_identifier = """PREFIX bf:
+PREFIX rdf:
+PREFIX rdfs:
+
+SELECT ?identifier
+WHERE {{
+ <{bf_instance}> a bf:Instance .
+ <{bf_instance}> bf:identifiedBy ?ident_bnode .
+ ?ident_bnode a bf:Local .
+ ?ident_bnode bf:source ?source_bnode .
+ ?ident_bnode rdf:value ?identifier .
+ ?source_bnode a bf:Source .
+ OPTIONAL {{
+ ?source_bnode rdfs:label "OColC" .
+ }}
+ OPTIONAL {{
+ ?source_bnode rdfs:label "OCLC" .
+ }}
+}}
+"""
+
+mode_of_issuance = """PREFIX bf:
+PREFIX rdf:
+PREFIX rdfs:
+
+SElECT ?mode_of_issuance
+WHERE {{
+ <{bf_instance}> a bf:Instance .
+ <{bf_instance}> bf:issuance ?mode_of_issuance .
+}}
+"""
+
+note = """PREFIX bf:
+PREFIX rdf:
+PREFIX rdfs:
+
+SELECT ?note
+WHERE {{
+ <{bf_instance}> a bf:Instance .
+ <{bf_instance}> bf:note ?note_bnode .
+ ?note_bnode a bf:Note .
+ ?note_bnode rdfs:label ?note .
+}}
+"""
+
+physical_description_dimensions = """PREFIX bf:
+PREFIX rdf:
+PREFIX rdfs:
+
+SELECT ?dimensions
+WHERE {{
+ <{bf_instance}> a bf:Instance .
+ <{bf_instance}> bf:dimensions ?dimensions .
+}}
+"""
+
+physical_description_extent = """PREFIX bf:
+PREFIX rdf:
+PREFIX rdfs:
+
+SELECT ?extent
+WHERE {{
+ <{bf_instance}> a bf:Instance .
+ <{bf_instance}> bf:extent ?extent_bnode .
+ ?extent_bnode a bf:Extent .
+ ?extent_bnode rdfs:label ?extent .
+}}
+"""
+
+place = """PREFIX bf:
+PREFIX rdf:
+PREFIX rdfs:
+
+SELECT ?place
+WHERE {{
+ <{bf_instance}> a bf:Instance .
+ <{bf_instance}> bf:provisionActivity ?activity .
+ ?activity a bf:Publication .
+ ?activity bf:place ?place_holder .
+ ?place_holder rdfs:label ?place .
+}}
+"""
+
+publisher = """PREFIX bf:
+PREFIX rdf:
+PREFIX rdfs:
+
+SELECT ?publisher
+WHERE {{
+ <{bf_instance}> a bf:Instance .
+ <{bf_instance}> bf:provisionActivity ?activity .
+ ?activity a bf:Publication .
+ ?activity bf:agent ?agent .
+ ?agent a bf:Agent .
+ ?agent rdfs:label ?publisher .
+}}
+"""
+
+title = """PREFIX bf:
+PREFIX rdf:
+PREFIX rdfs:
+
+SELECT ?main_title ?subtitle ?part_number ?part_name
+WHERE {{
+ <{bf_instance}> a bf:Instance .
+ <{bf_instance}> bf:title ?title .
+ ?title a {bf_class} .
+ ?title bf:mainTitle ?main_title .
+ OPTIONAL {{
+ ?title bf:subtitle ?subtitle .
+ }}
+ OPTIONAL {{
+ ?title bf:partNumber ?part_number .
+ }}
+ OPTIONAL {{
+ ?title bf:partName ?part_name
+ }}
+}}
+"""
diff --git a/ils_middleware/tasks/folio/mappings/bf_work.py b/ils_middleware/tasks/folio/mappings/bf_work.py
new file mode 100644
index 0000000..a73ab47
--- /dev/null
+++ b/ils_middleware/tasks/folio/mappings/bf_work.py
@@ -0,0 +1,80 @@
+contributor = """PREFIX bf:
+PREFIX bflc:
+PREFIX rdf:
+PREFIX rdfs:
+
+SELECT ?agent ?role
+WHERE {{
+ <{bf_work}> a bf:Work .
+ <{bf_work}> bf:contribution ?contrib_bnode .
+ ?contrib_bnode a bf:Contribution .
+ ?contrib_bnode bf:role ?role_uri .
+ ?role_uri rdfs:label ?role .
+ ?contrib_bnode bf:agent ?agent_uri .
+ ?agent_uri a {bf_class} .
+ ?agent_uri rdfs:label ?agent .
+}}
+"""
+
+editions = """PREFIX bf:
+PREFIX rdf:
+PREFIX rdfs:
+
+SELECT ?edition
+WHERE {{
+ <{bf_work}> a bf:Work .
+ <{bf_work}> bf:editionStatement ?edition .
+}}
+"""
+
+instance_type_id = """PREFIX bf:
+
+SELECT ?instance_type_id
+WHERE {{
+ <{bf_work}> a bf:Work .
+ <{bf_work}> bf:content ?instance_type .
+ ?instance_type rdfs:label ?instance_type_id .
+}}
+"""
+
+language = """PREFIX bf:
+
+SELECT ?language
+WHERE {{
+ <{bf_work}> a bf:Work .
+ <{bf_work}> bf:language ?language_uri .
+ ?language_uri rdfs:label ?language .
+}}
+"""
+
+primary_contributor = """PREFIX bf:
+PREFIX bflc:
+PREFIX rdf:
+PREFIX rdfs:
+
+SELECT ?agent ?role
+WHERE {{
+ <{bf_work}> a bf:Work .
+ <{bf_work}> bf:contribution ?contrib_bnode .
+ ?contrib_bnode a bflc:PrimaryContribution .
+ ?contrib_bnode bf:role ?role_uri .
+ ?role_uri rdfs:label ?role .
+ ?contrib_bnode bf:agent ?agent_uri .
+ ?agent_uri a {bf_class} .
+ ?agent_uri rdfs:label ?agent .
+}}
+"""
+
+subject = """PREFIX bf:
+PREFIX rdf:
+PREFIX rdfs:
+
+SELECT ?subject
+WHERE {{
+ <{bf_work}> a bf:Work .
+ <{bf_work}> bf:subject ?subject_node .
+ OPTIONAL {{
+ ?subject_node rdfs:label ?subject .
+ }}
+}}
+"""
diff --git a/tests/fixtures/bf.ttl b/tests/fixtures/bf.ttl
new file mode 100644
index 0000000..a5dbd15
--- /dev/null
+++ b/tests/fixtures/bf.ttl
@@ -0,0 +1,143 @@
+@prefix bf: .
+@prefix bflc: .
+@prefix rdf: .
+@prefix rdfs: .
+@prefix sinopia: .
+
+ rdfs:label "Informational works@en"@eng .
+
+ a bf:Person ;
+ rdfs:label "Brioni, Simone"@eng .
+
+ rdfs:label "Venice (Italy)"@eng .
+
+ rdfs:label "Italy@en"@eng .
+
+ a bf:Person ;
+ rdfs:label "Ramzanali Fazel, Shirin"@eng .
+
+ a bf:Person ;
+ rdfs:label "Blow, C. Joe"@eng .
+
+ rdfs:label "Islamophobia@en"@eng .
+
+ rdfs:label "Cultural pluralism@en"@eng .
+
+ rdfs:label "Sex discrimination against women@en"@eng .
+
+bf:Electronic rdfs:label "Electronic"@eng .
+
+bf:Text rdfs:label "Text"@eng .
+
+bf:Work rdfs:label "http://id.loc.gov/ontologies/bibframe/Work" .
+
+ rdfs:label "online resource"@eng .
+
+ rdfs:label "Text"@eng .
+
+ rdfs:label "single unit"@eng .
+
+ rdfs:label "Italian" .
+
+ rdfs:label "computer"@eng .
+
+ rdfs:label "incorrect" .
+
+ rdfs:label "Stanford University"@eng .
+
+ rdfs:label "Resource Description and Access"@eng .
+
+ rdfs:label "Resource Description and Access"@eng .
+
+ a bf:Instance ;
+ rdfs:label "OCLC"@eng ;
+ bflc:relationship [ a bflc:Relationship ;
+ bf:hasEquivalent "Also issued in print"@eng ] ;
+ bf:adminMetadata ;
+ bf:carrier ;
+ bf:copyrightDate "©2020" ;
+ bf:dimensions "30 cm by 15 cm"@eng ;
+ bf:editionStatement "1a edizione"@ita ;
+ bf:electronicLocator ,
+ ;
+ bf:extent [ a bf:Extent ;
+ rdfs:label "1 online resource (128 pages)"@eng ] ;
+ bf:hasItem ;
+ bf:identifiedBy [ a bf:Isbn ;
+ bf:qualifier "(ebook)"@eng ;
+ rdf:value "9788869694110" ],
+ [ a bf:Isbn ;
+ bf:qualifier "(print)"@eng ;
+ bf:status ;
+ rdf:value "9788869694103"@eng ],
+ [ a bf:Local ;
+ bf:source [ a bf:Source ;
+ rdfs:label "OCLC"@eng ] ;
+ rdf:value "1272909598"@eng ] ;
+ bf:instanceOf ;
+ bf:issuance ;
+ bf:media ;
+ bf:note [ a bf:Note ;
+ rdfs:label "Includes bibliographical references (page 117-128)"@eng ],
+ [ a bf:Note ;
+ rdfs:label "Description based on online resource (Stanford Digital Repository, viewed October 1, 2021)"@eng ] ;
+ bf:provisionActivity [ a bf:Publication ;
+ bf:agent [ a bf:Agent ;
+ rdfs:label "Edizioni Ca'Foscari"@eng ] ;
+ bf:date "2020"@eng ;
+ bf:place ] ;
+ bf:provisionActivityStatement "Venezia : Edizioni Ca'Foscari, 2020"@ita ;
+ bf:responsibilityStatement "Simone Brioni e Shirin Ramzanali Fazel"@ita ;
+ bf:seriesStatement "Diaspore, 2610-9387 ; 13"@ita ;
+ bf:title [ a bf:Title ;
+ bf:mainTitle "Scrivere di Islam"@ita ;
+ bf:note [ a bf:Note ;
+ rdfs:label " Title from PDF title page "@eng ] ;
+ bf:subtitle "raccontere la diaspora"@ita ],
+ [ a bf:ParallelTitle ;
+ bf:mainTitle "Writing about Islam"@eng ;
+ bf:subtitle "narrating a diaspora"@eng ] ;
+ sinopia:hasResourceTemplate "pcc:bf2:Monograph:Instance" ;
+
+ rdf:type bf:Electronic .
+
+ a bf:Text,
+ bf:Work ;
+ rdfs:label "\"Scrivere di Islam. Raccontare la diaspora (Writing About Islam. Narrating a Diaspora) is a meditation on our multireligious, multicultural, and multilingual reality. It is the result of a personal and collaborative exploration of the necessity to rethink national culture and identity in a more diverse, inclusive, and anti-racist way. The central part of this volume - both symbolically and physically - includes Shirin Ramzanali Fazel's reflections on the discrimination of Muslims, and especially Muslim women, in Italy and the UK. Looking at school textbooks, newspapers, TV programs, and sharing her own personal experience, this section invites us to change the way Muslim immigrants are narrated in scholarly research and news reports. Most importantly, this section urges us to consider minorities not just as 'topics' of cultural analysis, but as audiences and cultural agents. Following Shirin's invitation to question prevailing modes of representations of immigrants, the volume continues with a dialogue between the co-authors and discusses how collaboration can be a way to avoid reproducing a 'colonial model' of knowledge production, in which the white male scholar takes as object of analysis the work of an African female writer. The last chapter also asserts that immigration literature cannot be approached with the same expectations and questions readers would have when reading 'canonised' texts. A new critical terminology is needed in order to understand the innovative linguistic choices and narrative forms that immigrant writers have invented in order to describe a reality that has lacked representation or which has frequently been misrepresented, especially in the discourse around the contemporary Muslim diaspora\"--Abstract"@eng ;
+ bf:adminMetadata ;
+ bf:classification [ a bf:ClassificationLcc ;
+ bf:classificationPortion "BP52.5"@eng ] ;
+ bf:content ;
+ bf:contribution [ a bflc:PrimaryContribution ;
+ bf:agent ,
+ ;
+ bf:role ],
+ [ a bf:Contribution ;
+ bf:agent ;
+ bf:role ] ;
+ bf:editionStatement "1st edition"@eng ;
+ bf:genreForm ;
+ bf:hasInstance ;
+ bf:language ;
+ bf:notation [ a bf:Script ;
+ rdfs:label "Latin"@eng ] ;
+ bf:originDate "2020"@eng ;
+ bf:originPlace ;
+ bf:subject ,
+ ,
+ ,
+ "Immigrants' writings--History and criticism"@eng ;
+ bf:summary [ a bf:Summary ;
+ rdfs:label "\"Scrivere di Islam. Raccontare la diaspora (Writing About Islam. Narrating a Diaspora) is a meditation on our multireligious, multicultural, and multilingual reality. It is the result of a personal and collaborative exploration of the necessity to rethink national culture and identity in a more diverse, inclusive, and anti-racist way. The central part of this volume - both symbolically and physically - includes Shirin Ramzanali Fazel's reflections on the discrimination of Muslims, and especially Muslim women, in Italy and the UK. Looking at school textbooks, newspapers, TV programs, and sharing her own personal experience, this section invites us to change the way Muslim immigrants are narrated in scholarly research and news reports. Most importantly, this section urges us to consider minorities not just as 'topics' of cultural analysis, but as audiences and cultural agents. Following Shirin's invitation to question prevailing modes of representations of immigrants, the volume continues with a dialogue between the co-authors and discusses how collaboration can be a way to avoid reproducing a 'colonial model' of knowledge production, in which the white male scholar takes as object of analysis the work of an African female writer. The last chapter also asserts that immigration literature cannot be approached with the same expectations and questions readers would have when reading 'canonised' texts. A new critical terminology is needed in order to understand the innovative linguistic choices and narrative forms that immigrant writers have invented in order to describe a reality that has lacked representation or which has frequently been misrepresented, especially in the discourse around the contemporary Muslim diaspora\"--Abstract"@eng ] ;
+ bf:supplementaryContent [ a bf:SupplementaryContent ;
+ rdfs:label "Includes bibliographical references"@eng ] ;
+ bf:title [ a bf:Title ;
+ bf:mainTitle "Scrivere di Islam "@ita ] ;
+ sinopia:hasResourceTemplate "pcc:bf2:Monograph:Work" .
+
+ rdfs:label "https://phaidra.cab.unipd.it/detail/o:445140?mycoll=o:432583"@eng .
+
+ rdfs:label "Stanford Digital Repository"@eng .
+
+ rdfs:label "Author" .
+
diff --git a/tests/tasks/folio/mappings/test_bf_instance.py b/tests/tasks/folio/mappings/test_bf_instance.py
new file mode 100644
index 0000000..27343cc
--- /dev/null
+++ b/tests/tasks/folio/mappings/test_bf_instance.py
@@ -0,0 +1,102 @@
+import pytest # noqa: F401
+import rdflib
+
+import ils_middleware.tasks.folio.mappings.bf_instance as bf_instance_map
+
+uri = "https://api.stage.sinopia.io/resource/b0319047-acd0-4f30-bd8b-98e6c1bac6b0"
+
+
+def test_date_of_publication(test_graph: rdflib.Graph):
+ sparql = bf_instance_map.date_of_publication.format(bf_instance=uri)
+ dates = [row[0] for row in test_graph.query(sparql)]
+
+ assert str(dates[0]).startswith("2020")
+
+
+def test_isbn(test_graph: rdflib.Graph):
+ sparql = bf_instance_map.identifier.format(bf_instance=uri, bf_class="bf:Isbn")
+
+ isbns = [row[0] for row in test_graph.query(sparql)]
+
+ assert str(isbns[0]).startswith("9788869694110")
+ assert str(isbns[1]).startswith("9788869694103")
+
+
+def test_media_format(test_graph: rdflib.Graph):
+ sparql = bf_instance_map.instance_format_category.format(bf_instance=uri)
+ media_formats = [row[0] for row in test_graph.query(sparql)]
+
+ assert str(media_formats[0]).startswith("http://id.loc.gov/vocabulary/mediaTypes/c")
+
+
+def test_carrier_term(test_graph: rdflib.Graph):
+ sparql = bf_instance_map.instance_format_term.format(bf_instance=uri)
+ terms = [row[0] for row in test_graph.query(sparql)]
+
+ assert str(terms[0]).startswith("http://id.loc.gov/vocabulary/carriers/cr")
+
+
+def test_local_idenitifier(test_graph: rdflib.Graph):
+ sparql = bf_instance_map.local_identifier.format(bf_instance=uri)
+ local_idents = [row[0] for row in test_graph.query(sparql)]
+
+ assert str(local_idents[0]).startswith("1272909598")
+
+
+def test_mode_of_issuance(test_graph: rdflib.Graph):
+ sparql = bf_instance_map.mode_of_issuance.format(bf_instance=uri)
+ modes = [row[0] for row in test_graph.query(sparql)]
+
+ assert str(modes[0]).startswith("http://id.loc.gov/vocabulary/issuance/mono")
+
+
+def test_note(test_graph: rdflib.Graph):
+ sparql = bf_instance_map.note.format(bf_instance=uri)
+ notes = [row[0] for row in test_graph.query(sparql)]
+
+ assert len(str(notes[0])) == 50
+ assert len(str(notes[1])) == 90
+
+
+def test_physical_description_dimensions(test_graph: rdflib.Graph):
+ sparql = bf_instance_map.physical_description_dimensions.format(bf_instance=uri)
+ dimensions = [row[0] for row in test_graph.query(sparql)]
+
+ assert str(dimensions[0]).startswith("30 cm by 15 cm")
+
+
+def test_physical_description_extent(test_graph: rdflib.Graph):
+ sparql = bf_instance_map.physical_description_extent.format(bf_instance=uri)
+ extents = [row[0] for row in test_graph.query(sparql)]
+
+ assert str(extents[0]).startswith("1 online resource (128 pages)")
+
+
+def test_place(test_graph: rdflib.Graph):
+ sparql = bf_instance_map.place.format(bf_instance=uri)
+ places = [row[0] for row in test_graph.query(sparql)]
+
+ assert str(places[0]).startswith("Venice (Italy)")
+
+
+def test_publisher(test_graph: rdflib.Graph):
+ sparql = bf_instance_map.publisher.format(bf_instance=uri)
+ publishers = [row[0] for row in test_graph.query(sparql)]
+
+ assert str(publishers[0]).startswith("Edizioni Ca'Foscari")
+
+
+def test_main_title(test_graph: rdflib.Graph):
+ sparql = bf_instance_map.title.format(bf_instance=uri, bf_class="bf:Title")
+ titles = [row for row in test_graph.query(sparql)]
+
+ assert str(titles[0][0]).startswith("Scrivere di Islam")
+ assert str(titles[0][1]).startswith("raccontere la diaspora")
+
+
+def test_parallel_title(test_graph: rdflib.Graph):
+ sparql = bf_instance_map.title.format(bf_instance=uri, bf_class="bf:ParallelTitle")
+ titles = [row for row in test_graph.query(sparql)]
+
+ assert str(titles[0][0]).startswith("Writing about Islam")
+ assert str(titles[0][1]).startswith("narrating a diaspora")
diff --git a/tests/tasks/folio/mappings/test_bf_work.py b/tests/tasks/folio/mappings/test_bf_work.py
new file mode 100644
index 0000000..0046122
--- /dev/null
+++ b/tests/tasks/folio/mappings/test_bf_work.py
@@ -0,0 +1,62 @@
+import pytest # noqa: F401
+import rdflib
+
+import ils_middleware.tasks.folio.mappings.bf_work as bf_work_map
+
+
+work_uri = "https://api.stage.sinopia.io/resource/c96d8b55-e0ac-48a5-9a9b-b0684758c99e"
+
+
+def test_contributor_author_person(test_graph: rdflib.Graph):
+ sparql = bf_work_map.contributor.format(bf_work=work_uri, bf_class="bf:Person")
+
+ contributors = [row for row in test_graph.query(sparql)]
+
+ assert str(contributors[0][0]).startswith("Ramzanali Fazel, Shirin")
+ assert str(contributors[0][1]).startswith("Author")
+
+
+def test_edition(test_graph: rdflib.Graph):
+ sparql = bf_work_map.editions.format(bf_work=work_uri)
+
+ editions = [row[0] for row in test_graph.query(sparql)]
+
+ assert str(editions[0]).startswith("1st edition")
+
+
+def test_instance_type_id(test_graph: rdflib.Graph):
+ sparql = bf_work_map.instance_type_id.format(bf_work=work_uri)
+
+ type_idents = [row[0] for row in test_graph.query(sparql)]
+
+ assert str(type_idents[0]).startswith("Text")
+
+
+def test_language(test_graph: rdflib.Graph):
+ sparql = bf_work_map.language.format(bf_work=work_uri)
+
+ languages = [row[0] for row in test_graph.query(sparql)]
+
+ assert str(languages[0]).startswith("Italian")
+
+
+def test_primary_contributor(test_graph: rdflib.Graph):
+ sparql = bf_work_map.primary_contributor.format(
+ bf_work=work_uri, bf_class="bf:Person"
+ )
+
+ primary_contributors = [row for row in test_graph.query(sparql)]
+
+ assert str(primary_contributors[0][0]).startswith("Brioni, Simone")
+ assert str(primary_contributors[0][1]).startswith("Author")
+
+ assert str(primary_contributors[1][0]).startswith("Blow, C. Joe")
+ assert str(primary_contributors[1][1]).startswith("Author")
+
+
+def test_subject(test_graph: rdflib.Graph):
+ sparql = bf_work_map.subject.format(bf_work=work_uri)
+
+ subjects = [row for row in test_graph.query(sparql)]
+
+ assert len(subjects) == 3