Assertional knowledge#

Binder

In an ontological framework, ontology entities are used as a knowledge representation form. Those can be further categorized in two groups: ontology individuals (assertional knowledge), and ontology classes, relationships, attributes and annotations (terminological knowledge). This page focuses on how to access, edit and navigate the assertional knowledge of an ontology using SimPhoNy.

Such functionality is presented in the form of a tutorial, in which the city namespace from SimPhoNy’s example City ontology, the emmo namespace from the Elementary Multiperspective Material Ontology (EMMO) are used as examples. If you want to follow the tutorial along, please make sure that both ontologies are installed. If it is not the case, you can install them by running the command below.

[1]:
# Install the ontologies
!pico install city emmo

Moreover, this tutorial concentrates on how to interact with ontology individual objects. Each ontology individual object represents a single individual in the ontology.

Instantiating ontology individuals#

On this page, examples are based exclusively on newly created ontology individuals. You can learn how to retrieve existing ontology individuals from a data source in the next sections.

To instantiate a new ontology individual, just call an ontology class object as shown below. If the words “ontology class object” sound new to you, please read the previous section.

Certain attributes of the ontology individual can already be set at creation time by passing their values as keyword arguments, where the keyword is any of the attribute labels or its namespace suffix. Such attributes are, specifically, the ones returned by the attributes property and the optional attributes property of the ontology class being called.

[2]:
from simphony_osp.namespaces import city, emmo, owl, rdfs, simphony

print(
    f'The following attributes of a new {city.Citizen} '
    f'individual can be set using keyword arguments:'
)
for attribute in set(city.Citizen.attributes) | city.Citizen.optional_attributes:
    print(f'  - {attribute}')

city.Citizen(name="Test Person", age=42)
The following attributes of a new Citizen individual can be set using keyword arguments:
  - name
  - age
[2]:
<OntologyIndividual: ea9cfe74-65e4-4ebc-ac27-8a66de82866b>

In fact, if any of the attributes is defined in the ontology as mandatory using ontology axioms, you will be forced to provide them in the function call (otherwise an exception will be raised).

Tip

In Python, you can pass keyword arguments with spaces or other characters not typically allowed in keyword arguments by unpacking a dictionary in the function call: city.Citizen(name="Test Person", **{"age": 42}).

Note

At the moment, it is not possible to instantiate multi-class individuals. We are aware of this issue, and planning to include this functionality in a future minor release.

Until this is fixed, the suggested workaround is to instantiate an ontology individual of any class and change the classes a posteriori, just as shown below.

person = owl.Thing()

person.classes = city.Citizen, emmo.Cogniser

By default, new ontology individuals are assigned a random IRI from the simphony-osp.eu domain.

[3]:
city.Citizen(name="Test Person", age=42).identifier
[3]:
rdflib.term.URIRef('https://www.simphony-osp.eu/entity#6b7cf472-9dbe-4d9e-93e2-0f56ee308d27')

However, it is possible to fix the identifier using the iri keyword argument.

[4]:
city.Citizen(
    name="Test Person", age=42,
    iri='http://example.org/entity#test_person'
).identifier
[4]:
rdflib.term.URIRef('http://example.org/entity#test_person')

An individual can also be instantiated in a session different from the default one using the session keyword argument (see the sessions section).

Ontology individual objects#

Ontology individuals are a special type of ontology entities, and thus, the ontology individual objects inherit from ontology entity objects, meaning that they share their functionality.

In SimPhoNy, an ontology individual is characterized by

  • the information about the ontology individual itself such as the classes it belongs to, its label and its attributes;

  • the connections to other ontology individuals.

Moreover, such information is stored on a so-called session (see next section).

As said, ontology individual objects inherit from ontology entity objects. Therefore, it is also possible to access their label, identifier, namespace and super- or subclasses. Below you can find an example. Head to the terminological knowledge section for more details.

Note

Even though ontology individual objects share the functionality of ontology entity objects, there are some slight differences to consider:

  • The namespace property tipically returns None, regardless of the IRI of the ontology individual. This happens because in order to belong to a namespace, an ontology entity needs not only to have an IRI that contains the namespace IRI, but also to belong to the same session. Ontologies installed with pico live in their own, separate session.

  • The superclasses, direct_superclasses, subclasses and direct_subclasses properties, as well as the is_subclass_of method refer to the superclasses and subclasses of all the classes the ontology individual belongs to, as illustrated in the example.

  • The properties label, label_lang, label_literal and session are writable. This means that both the main label of ontology individuals can be changed and the individuals themselves may be moved from one session to another by changing the value of such properties.

[5]:
person = emmo.Cogniser()
# Instantiate an ontology individual of class Cogniser. According to the EMMO's
# documentation, a Cogniser is defined as:
# > An interpreter who establish the connection between an icon an an object
# > recognizing their resemblance (e.g. logical, pictorial)
# The following example for a Cogniser is provided:
# > The scientist that connects an equation to a physical phenomenon.

person.label, person.label_lang = "My neighbor", "en"

print("Label:", f"{person.label} ({person.label_lang})", end='\n'*2)
print("Label literal:", person.label_literal.__repr__(), end='\n'*2)
print("List of labels:", list(person.iter_labels()).__repr__(), end='\n'*2)
print("Identifier:", person.identifier.__repr__(), end='\n'*2)
print("Namespace:", person.namespace.__repr__(), end='\n'*2)

print('Superclasses:', person.superclasses, end='\n'*2)
print('Subclasses:', person.subclasses, end='\n'*2)
print('Direct superclasses:', person.direct_superclasses, end='\n'*2)
print('Direct subclasses:', person.direct_subclasses, end='\n'*2)

print("Does any of the classes of the individual belong the \"Semiotics\" branch of EMMO?", person.is_subclass_of(emmo.Semiotics))

from simphony_osp.ontology import OntologyIndividual
print("\nIs the entity an individual?", isinstance(person, OntologyIndividual))
Label: My neighbor (en)

Label literal: rdflib.term.Literal('My neighbor', lang='en')

List of labels: [rdflib.term.Literal('My neighbor', lang='en')]

Identifier: rdflib.term.URIRef('https://www.simphony-osp.eu/entity#03659fa1-5c91-44a0-a73c-f475d3b328fe')

Namespace: None

Superclasses: frozenset({<OntologyClass: CausalObject http://emmo.info/emmo#EMMO_c5ddfdba_c074_4aa4_ad6b_1ac4942d300d>, <OntologyClass: Semiotics http://emmo.info/emmo#EMMO_8bb6b688_812a_4cb9_b76c_d5a058928719>, <OntologyClass: EMMO http://emmo.info/emmo#EMMO_802d3e92_8770_4f98_a289_ccaaab7fdddf>, <OntologyClass: Item http://emmo.info/emmo#EMMO_eb3a768e_d53e_4be9_a23b_0714833c36de>, <OntologyClass: CausalSystem http://emmo.info/emmo#EMMO_e7aac247_31d6_4b2e_9fd2_e842b1b7ccac>, <OntologyClass: SemioticEntity http://emmo.info/emmo#EMMO_b803f122_4acb_4064_9d71_c1e5fd091fc9>, <OntologyClass: Interpreter http://emmo.info/emmo#EMMO_0527413c_b286_4e9c_b2d0_03fb2a038dee>, <OntologyClass: http://www.w3.org/2002/07/owl#Thing>, <OntologyClass: Perspective http://emmo.info/emmo#EMMO_49267eba_5548_4163_8f36_518d65b583f9>, <OntologyClass: Cogniser http://emmo.info/emmo#EMMO_19608340_178c_4bfd_bd4d_0d3b935c6fec>})

Subclasses: frozenset({<OntologyClass: Cogniser http://emmo.info/emmo#EMMO_19608340_178c_4bfd_bd4d_0d3b935c6fec>})

Direct superclasses: frozenset({<OntologyClass: Interpreter http://emmo.info/emmo#EMMO_0527413c_b286_4e9c_b2d0_03fb2a038dee>})

Direct subclasses: frozenset()

Does any of the classes of the individual belong the "Semiotics" branch of EMMO? True

Is the entity an individual? True

In addition, ontology individuals have extra functionality that is specific to them.

For example, there is an extra method to verify whether they are an instance of a specific ontology class (which is just an alias for is_subclass_of).

[6]:
person.is_a(emmo.Semiotics)
[6]:
True

It is also is possible not only to verify the classes that the individual belongs to,

[7]:
person.classes
[7]:
frozenset({<OntologyClass: Cogniser http://emmo.info/emmo#EMMO_19608340_178c_4bfd_bd4d_0d3b935c6fec>})

but also to change them.

[8]:
person.classes = city.Citizen, emmo.Cogniser

person.classes
[8]:
frozenset({<OntologyClass: Citizen https://www.simphony-osp.eu/city#Citizen>,
           <OntologyClass: Cogniser http://emmo.info/emmo#EMMO_19608340_178c_4bfd_bd4d_0d3b935c6fec>})

To get the session an individual belongs to, use the session property. Remember that this property can be also changed in order to transfer the individual from one session to another.

[9]:
person.session
[9]:
<simphony_osp.session.session.Session at 0x7f7855549fa0>

Managing attributes, relationships and annotations#

Using the index operator []#

SimPhoNy features a single, unified syntax based on the Python index [] operator to manage the relationships between ontology individuals, the values of the attributes of an individual, and the values of ontology annotations.

For example, assume one wants to create a city with several neighborhoods and inhabitants. The first step is to instantiate the ontology individuals that represent such elements.

[10]:
freiburg = city.City(name="Freiburg", coordinates=[47.997791, 7.842609])

neighborhoods = {
    city.Neighborhood(name=name, coordinates=coordinates)
    for name, coordinates in [
        ('Altstadt', [47.99525, 7.84726]),
        ('Stühlinger', [47.99888, 7.83774]),
        ('Neuburg', [48.00021, 7.86084]),
        ('Herdern', [48.00779, 7.86268]),
        ('Brühl', [48.01684, 7.843]),
    ]
}

citizen_1 = city.Citizen(name='Nikola', age=35)
citizen_2 = city.Citizen(name='Lena', age=70)

The next step is connecting them, modifying the values of their attributes and adding annotations.

Let’s start trying to declare that the neighborhoods are part of the city and that the citizens are inhabitants of the city using the city.hasPart and city.hasInhabitant relationships.

The individuals that are already connected to the city through this relationship can be consulted as follows.

[11]:
freiburg[city.hasPart]
[11]:
set() <has part of ontology individual 1fa8d37a-d5de-42f9-ace1-8009c506bd8c>

The above statement yields a relationship set object. Relationship sets are set-like objects that manage the ontology individuals that are linked to the given individual and relationship (in this example, freiburg and city.hasPart). You will notice in the following examples, that relationship set objects have a few extra capabilities that Python sets do not have that make the interaction with them more natural.

Note

Set-like objects are objects compatible with the standard Python sets, meaning that all the methods and functionality from Python sets are available for set-like objects.

In order to attach items through the given relationship, all that is needed is an in-place set union.

[12]:
freiburg[city.hasPart] | neighborhoods  # does not attach the neighborhoods
print(freiburg[city.hasPart])
freiburg[city.hasPart] |= neighborhoods  # attaches the neighborhoods (in-place union)
print(freiburg[city.hasPart])

freiburg[city.hasInhabitant] += citizen_1, citizen_2  # attaches the citizens
# the '+=` operator is not available in standard Python sets and is a shorthand for
# the following operations:
# - `+= citizen_1, citizen_2` is equivalent to `|= {citizen_1, citizen_2}`
# - `+= {citizen_1, citizen_2}` is equivalent to `|= {citizen_1, citizen_2}`
# - `+= [citizen_1, citizen_2]` raises a TypeError (this shortcut only works for tuples and set-like objects)
# -  `+= citizen_3` is equivalent to `|= {citizen_3}
# the `-=` operator is avilable in standard Python sets, but has been extended
# to work like in the above examples when used together with non set-like objects.
set()
{<OntologyIndividual: 5b326b26-0919-46dd-a07f-9d76f2839835>, <OntologyIndividual: 83b21a8e-060c-4530-a1cf-18bf84c7511a>, <OntologyIndividual: f7c7e3dd-bdd9-4d17-a365-b2d07ac3202f>, <OntologyIndividual: 05b864f1-cec9-486f-98b4-1c802d51261e>, <OntologyIndividual: 2604c106-45f3-4534-9ce3-2bc313469918>}

Exactly in the same way, when ontology attributes or ontology annotations are passed to the index operator [], attribute sets and annotation sets are spawned, which behave similarly to relationship sets.

[13]:
# ATTRIBUTES
# - assign one more name to Lena
citizen_2[city['name']] += 'Helena'
print(citizen_2[city['name']].__repr__(), end='\n'*2)

# - change the age of Lena (`=` replaces all the values of the attribute)
print(citizen_2[city.age].__repr__())
citizen_2[city.age] = 55
print(citizen_2[city.age].__repr__())

# ANNOTATIONS
citizen_1[rdfs.comment] = (
    'Lena was born in Berlin, but moved to Freiburg when she was 28 years old.',
    'She likes to go into the woods and get lost in her thoughts.'
)
print(citizen_1[rdfs.comment].__repr__())
{'Lena', 'Helena'} <name of ontology individual 605663a6-0642-4a6a-82b6-cc7f5e04ab9b>

{70} <age of ontology individual 605663a6-0642-4a6a-82b6-cc7f5e04ab9b>
{55} <age of ontology individual 605663a6-0642-4a6a-82b6-cc7f5e04ab9b>
{'Lena was born in Berlin, but moved to Freiburg when she was 28 years old.', 'She likes to go into the woods and get lost in her thoughts.'} <http://www.w3.org/2000/01/rdf-schema#comment of ontology individual cbc45216-955a-4eee-993e-d9169e627128>

Note

In SimPhoNy, relationships, attributes and annotations are treated in an ontological sense. This means that when using the corresponding Python object to access or modify them, one is referring not only to such ontology entity, but also to all of its subclasses. You can verify this fact noting that freiburg[owl.topObjectProperty] returns all individuals attached to freiburg, as all relationships are a subclass of owl:topObjectProperty.

Strings can also be used with the index notation [] as a shorthand in certain cases

  • to access the attributes of any of the classes that the individual belongs to, or other attributes that have already been assigned to the individual,

  • to access relationships that have already been used to link the inidividual to others,

  • to access annotations whose value has been already assigned.

[14]:
freiburg["hasInhabitant"]
[14]:
{<OntologyIndividual: 605663a6-0642-4a6a-82b6-cc7f5e04ab9b>, <OntologyIndividual: cbc45216-955a-4eee-993e-d9169e627128>} <has inhabitant of ontology individual 1fa8d37a-d5de-42f9-ace1-8009c506bd8c>
[15]:
citizen_1["age"]
[15]:
{35} <age of ontology individual cbc45216-955a-4eee-993e-d9169e627128>

Therefore the most relevant use-case of passing strings is accessing information from existing individuals, rather than constructing new ones.

Tip

The index notation [] supports IPython autocompletion for strings. When working on a Jupyter notebook, it is possible to get suggestions for the strings that will work for that specific individual by writing individual[" and pressing TAB.

Even though in this example only a few possibilities of the relationship-, attribute- and annotation sets have been covered, remember that they are compatible with standard Python sets. So hopefully, this introduction should be enough to consider the remaining possibilities on your own: remove elements with -=, check if a certain relationship is being used if freiburg[city.hasInhabitant]:, loop over elements for connected_individual in freiburg[city.hasInhabitant]:, etc.

del freiburg[city.hasInhabitant] and freiburg[city.hasInhabitant] = None can also be used and are equivalent to freiburg[city.hasInhabitant] = set().

When it comes to accessing single values from a relationship-, attribute- or annotation set, there are three built-in shortcuts to make it easier than iterating over them:

  • any() returns an element from the set in a non-deterministic way. Returns None if the set is empty.

  • one() returns the single element in the set. If the set is empty or has multiple elements, thein the exceptions ResultEmptyError or MultipleResultsError are respectively raised.

  • all() returns the set itself, and is therefore redundant. Can be used to improve code readability if needed.

[16]:
# print(citizen_2['name'].one())  # Raises `MultipleResultsError`, as Lena has multiple names.
print(citizen_1['name'].any())
print(citizen_1['name'].all())

# print(citizen_2[city.hasChild].one())  # Raises `ResultEmptyError`, as Nikola has not been declared to have children.
print(citizen_2[city.hasChild].any())
print(citizen_2[city.hasChild].all().__repr__())
Nikola
{'Nikola'}
None
set() <has child of ontology individual 605663a6-0642-4a6a-82b6-cc7f5e04ab9b>

Finally, if it is needed to find individuals that are connected through an inverse relationship, the .inverse attribute of the relationship sets can be used.

[17]:
citizen_1[city.hasInhabitant].inverse.one()['name'].one()
[17]:
'Freiburg'

Using the Python dot notation (attributes only)#

The Python dot notation can be used to access and set the attributes of individuals in all cases when both strings can be passed to the index notation [] and the string is compatible with the Python syntax (e.g. it contains no spaces). See the previous section for more details.

[18]:
citizen_1.name, citizen_1.age
[18]:
('Nikola', 35)
[19]:
citizen_1.age = 34
citizen_1.age
[19]:
34
[20]:
citizen_1.attributes
[20]:
mappingproxy({<OntologyAttribute: name https://www.simphony-osp.eu/city#name>: frozenset({'Nikola'}),
              <OntologyAttribute: age https://www.simphony-osp.eu/city#age>: frozenset({34})})

Tip

The dot notation also supports IPython autocompletion.

The dot notation is limited to attributes with a single value. When several values are assigned to the same attribute, a RuntimeError is raised.

It is possible to get a dictionary with all the attributes of an individual and its values using the attributes attribute.

[21]:
citizen_2.attributes
[21]:
mappingproxy({<OntologyAttribute: name https://www.simphony-osp.eu/city#name>: frozenset({'Helena',
                         'Lena'}),
              <OntologyAttribute: age https://www.simphony-osp.eu/city#age>: frozenset({55})})

Using the get, iter, connect, and disconnect methods (relationships only)#

The method connect connects individuals using the given relationship.

[22]:
# remove the existing connections between Freiburg and its citizens
del freiburg[city.hasInhabitant]
print(freiburg[city.hasInhabitant].__repr__())

# use the connect method to restore them
freiburg.connect(citizen_1, citizen_2, rel=city.hasInhabitant)
print(freiburg[city.hasInhabitant].__repr__())
set() <has inhabitant of ontology individual 1fa8d37a-d5de-42f9-ace1-8009c506bd8c>
{<OntologyIndividual: 605663a6-0642-4a6a-82b6-cc7f5e04ab9b>, <OntologyIndividual: cbc45216-955a-4eee-993e-d9169e627128>} <has inhabitant of ontology individual 1fa8d37a-d5de-42f9-ace1-8009c506bd8c>

The method disconnect disconnects ontology individuals. Optionally a relationship and class filter can be given.

[23]:
citizen_3 = city.Citizen(name='Lukas', age=2)
citizen_1.connect(citizen_3, rel=city.hasChild)
print(citizen_1[city.hasChild])

citizen_1.disconnect(citizen_3)  # disconnects citizen_3
print(citizen_1[city.hasChild])

citizen_1.connect(citizen_3, rel=city.hasChild)

citizen_1.disconnect(rel=city.worksIn)  # does not disconnect citizen_3, as the relationship does not match the filter
print(citizen_1[city.hasChild])
citizen_1.disconnect(rel=city.hasChild, oclass=city.Building)  # does not disconnect citizen_3, as the its class does not match the filter
print(citizen_1[city.hasChild])
citizen_1.disconnect(citizen_3, oclass=city.Citizen)  # disconnect works, as the filters match now
print(citizen_1[city.hasChild])
{<OntologyIndividual: f57f53e1-95b4-4fd2-9272-c5c9a44c42f2>}
set()
{<OntologyIndividual: f57f53e1-95b4-4fd2-9272-c5c9a44c42f2>}
{<OntologyIndividual: f57f53e1-95b4-4fd2-9272-c5c9a44c42f2>}
set()

The method get is used to obtain the individuals linked through a given relationship. Filters to restrict the results only to specific individuals, relationships and classes, as well as any combination of them can optinally be provided. The iter method behaves similarly, but returns an interator instead.

[24]:
print(freiburg.get().__repr__(), end='\n'*2)  # returns everything attached to Freiburg (a relationship set)

print(freiburg.get(rel=city.hasInhabitant).__repr__(), end='\n'*2)  # returns only the citizens (a relationship set)

print(freiburg.get(oclass=city.Citizen).__repr__(), end='\n'*2)  # also returns only the citizens (a relationship set)

# filtering specific individuals (can be combined with class and relationship filters)
print(freiburg.get(citizen_1).__repr__())
print(freiburg.get(citizen_1, citizen_2).__repr__())
print(freiburg.get(citizen_1.identifier).__repr__())
print(freiburg.get('https://example.org/city#unknown_citizen').__repr__())
print(freiburg.get(citizen_1, rel=city.hasChild).__repr__())
print(freiburg.get(citizen_1, rel=city.hasInhabitant).__repr__())
print(freiburg.get(citizen_1, rel=city.hasInhabitant, oclass=city.Building).__repr__())
{<OntologyIndividual: 605663a6-0642-4a6a-82b6-cc7f5e04ab9b>, <OntologyIndividual: 5b326b26-0919-46dd-a07f-9d76f2839835>, <OntologyIndividual: cbc45216-955a-4eee-993e-d9169e627128>, <OntologyIndividual: 83b21a8e-060c-4530-a1cf-18bf84c7511a>, <OntologyIndividual: f7c7e3dd-bdd9-4d17-a365-b2d07ac3202f>, <OntologyIndividual: 05b864f1-cec9-486f-98b4-1c802d51261e>, <OntologyIndividual: 2604c106-45f3-4534-9ce3-2bc313469918>} <http://www.w3.org/2002/07/owl#topObjectProperty of ontology individual 1fa8d37a-d5de-42f9-ace1-8009c506bd8c>

{<OntologyIndividual: 605663a6-0642-4a6a-82b6-cc7f5e04ab9b>, <OntologyIndividual: cbc45216-955a-4eee-993e-d9169e627128>} <has inhabitant of ontology individual 1fa8d37a-d5de-42f9-ace1-8009c506bd8c>

{<OntologyIndividual: 605663a6-0642-4a6a-82b6-cc7f5e04ab9b>, <OntologyIndividual: cbc45216-955a-4eee-993e-d9169e627128>} <http://www.w3.org/2002/07/owl#topObjectProperty of ontology individual 1fa8d37a-d5de-42f9-ace1-8009c506bd8c>

<OntologyIndividual: cbc45216-955a-4eee-993e-d9169e627128>
(<OntologyIndividual: cbc45216-955a-4eee-993e-d9169e627128>, <OntologyIndividual: 605663a6-0642-4a6a-82b6-cc7f5e04ab9b>)
<OntologyIndividual: cbc45216-955a-4eee-993e-d9169e627128>
None
None
<OntologyIndividual: cbc45216-955a-4eee-993e-d9169e627128>
None

Using the get and iter methods, it is also possible to discover the specific relationships that connect two individuals when a superclass of them is given.

[25]:
freiburg.get(rel=owl.topObjectProperty, return_rel=True)
[25]:
((<OntologyIndividual: 5b326b26-0919-46dd-a07f-9d76f2839835>,
  <OntologyRelationship: has part https://www.simphony-osp.eu/city#hasPart>),
 (<OntologyIndividual: 83b21a8e-060c-4530-a1cf-18bf84c7511a>,
  <OntologyRelationship: has part https://www.simphony-osp.eu/city#hasPart>),
 (<OntologyIndividual: f7c7e3dd-bdd9-4d17-a365-b2d07ac3202f>,
  <OntologyRelationship: has part https://www.simphony-osp.eu/city#hasPart>),
 (<OntologyIndividual: 05b864f1-cec9-486f-98b4-1c802d51261e>,
  <OntologyRelationship: has part https://www.simphony-osp.eu/city#hasPart>),
 (<OntologyIndividual: 2604c106-45f3-4534-9ce3-2bc313469918>,
  <OntologyRelationship: has part https://www.simphony-osp.eu/city#hasPart>),
 (<OntologyIndividual: 605663a6-0642-4a6a-82b6-cc7f5e04ab9b>,
  <OntologyRelationship: has inhabitant https://www.simphony-osp.eu/city#hasInhabitant>),
 (<OntologyIndividual: cbc45216-955a-4eee-993e-d9169e627128>,
  <OntologyRelationship: has inhabitant https://www.simphony-osp.eu/city#hasInhabitant>))

Operations#

Operations are actions (written in Python) that can be executed on instances of specific ontology classes that they are defined for.

A great example of the applications of operations is the interaction with file objects in SimPhoNy wrappers that support it, for example, the included dataspace wrapper.

[26]:
from pathlib import Path
from tempfile import TemporaryDirectory
from urllib import request

from IPython.display import Image

from simphony_osp.wrappers import Dataspace

dataspace_directory = TemporaryDirectory()
example_directory = TemporaryDirectory()

# Download a picture of Freiburg using urllib
# from _Visit Freiburg_ - https://visit.freiburg.de
url = (
    "https://visit.freiburg.de/extension/portal-freiburg"
    "/var/storage/images/media/bibliothek/teaser-bilder-startseite"
    "/freiburg-kunst-kultur-copyright-fwtm-polkowski/225780-1-ger-DE"
    "/freiburg-kunst-kultur-copyright-fwtm-polkowski_grid_medium.jpg"
)
file, response = request.urlretrieve(url)

# Open a dataspace session in a temporary directory
with Dataspace(dataspace_directory.name, True) as session:
    # Create an individual belonging to SimPhoNy's file class
    picture = simphony.File(
        iri='http://example.org/freiburg#my_picture'
    )

    # Use the `upload` operation to assign data to the file object
    picture.operations.upload(file)

    # Commit the changes
    session.commit()

# Access the saved data and retrieve the Picture using the `download` operation
with Dataspace(dataspace_directory.name, True) as session:
    picture = session.from_identifier('http://example.org/freiburg#my_picture')
    download_path = Path(example_directory.name) / 'my_picture.jpg'
    picture.operations.download(download_path)

# Uncomment this line to show the downloaded picture
# (you can do so by running the tutorial yourself using Binder)
# Image(download_path)