You should probably read an introduction to Topic Maps before reading this. We recommend Steve Pepper’s The TAO of Topic Maps.
Also check out the top-level tutorial for how to do most of these things through the browser which is how non-developers should do them.
You can get an interpreter by running the python wrapper script inside the bin folder like this:
$ bin/python
First we need to create a topic map:
>>> import ztm.topicmaps
>>> topicmap = ztm.topicmaps.topicmap.TopicMap()
The topic map is a container for topics and associations. While the map itself has no meaning on its own in Topic Maps terms, in ztm.topicmaps it is a central object. Particularly for searching and indexes.
Note
Below you will see __reprname__ arguments specified each time a new topic is created. This is just to get nicer representations we can test deterministicly and can be ignored.
Lets create a topic and give it a type:
>>> alpharomeo = topicmap.createTopic(__reprname__='alpharomeo')
>>> car = topicmap.createTopic(__reprname__='car')
>>> alpharomeo.addType(car)
You can also do the inverse and add instances to the type:
>>> ferrari = topicmap.createTopic(__reprname__='ferrari')
>>> car.addInstance(ferrari)
The core API tries to be symmetrical. You should be able to get a feel for what methods are available after a short while. If not, we haven’t done a good enough job.
By now there should be some topics in the topic map:
>>> topicmap.topiccount
3
>>> car.instancecount
2
>>> alpharomeo in car.instances
True
You can find all current topic types via topictypes attribute the topic map.
>>> topicmap.topictypes
frozenset([<Topic car>])
>>> alpharomeo in topicmap.topictypes
False
Topics can have super and subtypes.
>>> vehicle = topicmap.createTopic(__reprname__='vehicle')
>>> vehicle.addSubType(car)
The API is symmetrical here as well.
>>> truck = topicmap.createTopic(__reprname__='truck')
>>> truck.addSuperType(vehicle)
>>> vehicle in truck.supertypes
True
>>> truck in vehicle.subtypes
True
>>> car in vehicle.subtypes
True
Lets add a truck first. You can send the type directly to the createTopic method.
>>> scania = topicmap.createTopic(types=(truck,), __reprname__='scania')
>>> vehicle.subtypes
frozenset([<Topic car>, <Topic truck>])
The indexes are aware of the super-subtype relationships:
>>> vehicle.allinstancecount
3
>>> scania in vehicle.allinstances
True
>>> scania in truck.allinstances
True
Warning
The instances attribute is the same as the directinstances and not the allinstances like other parts of the API.
Note
Most methods that take references to topics also take serial numbers and strings to match subject identifiers.
When the input is a string the methods will try to do something logically similar to this:
def somemethod(self, topic):
if isinstance(topic, (basestring, int, long)):
topic = self.topicmap.getTopic(topic)
...
Topics can be given identities for lookup and merging.
>>> car.addSubjectIdentifier(u'http://psi.example.com/car')
'http://psi.example.com/car'
>>> topicmap.getTopic(u'http://psi.example.com/car')
<Topic car>
>>> car.removeSubjectIdentifier(u'http://psi.example.com/car')
'http://...
>>> topicmap.getTopic(u'http://psi.example.com/car')
...
TopicNotFoundError: http://psi.example.com/car
Lets give our topics some names.:
>>> __ = vehicle.createName('Vehicle')
>>> __ = truck.createName('Truck')
>>> __ = car.createName('Car')
>>> __ = alpharomeo.createName('Alpha Romeo')
>>> __ = ferrari.createName('Ferrari')
>>> name = scania.createName('Scania')
>>> name.value
u'Scania'
Names are first class objects in ztm.topicmaps.
All names have a type, by default that is a topic with a subject identifier of http://psi.topicmaps.org/iso13250/topic-name. It gets created automatically the first time a name is created without a type.
>>> name.type
<Topic http://psi.topicmaps.org/iso13250/model/topic-name>
>>> # Lets give it a nicer repr to avoid loooong lines later
>>> name.type.__name__ = 'topic-name'
>>> repr(name.type)
'<Topic topic-name>'
Lets create a new name type. (A bit silly example, but still... it does the job.)
>>> nom_de_tutorial = topicmap.createTopic(__reprname__='nom_de_tutorial')
>>> nom = scania.createName(u'Test', type=nom_de_tutorial)
>>> nom.type
<Topic nom_de_tutorial>
>>> scania.getNameValue(nom_de_tutorial)
u'Test'
Name types can be found on the nametypes attribute on the topic map object same as topic types were above.
>>> topicmap.nametypes
frozenset([<Topic topic-name>, <Topic nom_de_tutorial>])
Names can be scoped like all other properties of a topic. Lets demonstrate this by adding a few language scopes.
>>> language = topicmap.createTopic(__reprname__='language')
>>> english = topicmap.createTopic(__reprname__='english', subjectidentifiers=ztm.topicmaps.psis.languages.en)
>>> english.addType(language)
>>> norwegian = topicmap.createTopic(__reprname__='norwegian')
>>> norwegian.addSubjectIdentifier(ztm.topicmaps.psis.languages.no)
'http://psi.oasis-open.org/iso/639/#nor'
>>> norwegian.addType(language)
In Norwegian the name for cars is Bil.
>>> norwegian_name = car.createName('Bil', scope=norwegian)
>>> norwegian in norwegian_name.scope.themes
True
>>> car.getName(scope=norwegian).value
u'Bil'
Note
The ztm.topicmaps.psis.langauges.no reference is just a dotted path to a unicode string and would be the equivalent of just writing:
norwegian.addSubjectIdentifier(u'http://psi.oasis-open.org/iso/639/#nor')
By convention we put subject identifiers often used or defined by the package in a file named psis.py. This way they can be changed in one location and we don’t have to copy them around, risking typos and other problems.
The most common use case is a single name.
>>> scania.getName(ztm.topicmaps.psis.model.topic_name).value
u'Scania'
But if there are multiple matching names they are sorted by their creation date and can be retrieved through the index parameter (0-based index):
>>> scania.listNames()
[<Name ...>, <Name ...>]
>>> # None indicates ANY name type here.
>>> scania.getName(None, index=1)
<Name u"Test" - nom_de_tutorial for topic scania in {}>
>>> # No argument for the type parameter defaults topic-name type
>>> scania.getName().value
u'Scania'
>>> scania.getName(ztm.topicmaps.psis.model.topic_name).value
u'Scania'
Names can be updated:
>>> name.value = u'New value'
>>> scania.getName(ztm.topicmaps.psis.model.topic_name).value
u'New value'
>>> name.value = u'Scania' # Put the name back
Names can be created if missing or updated if present:
>>> name2 = scania.setName(u'Value', create=True, index=1)
>>> name2.value
u'Value'
>>> scania.setName(u'Revalue', create=True, index=1)
<Name u"Revalue" - topic-name for topic scania in {}>
>>> _.value
u'Revalue'
>>> name2.delete()
When looking up names, occurrences and roles by their type the API will also try to check among subtypes.
>>> fullname = topicmap.createTopic(__reprname__='fullname') >>> fullname.addSuperType(ztm.topicmaps.psis.model.topic_name) >>> ferrari.createName(u'Ferrari S.p.A', type=fullname) <Name u"Ferrari S.p.A" - fullname for topic ferrari in {}>>>> from pprint import pprint >>> pprint(ferrari.listNames(type=ztm.topicmaps.psis.model.topic_name)) [<Name u"Ferrari" - topic-name for topic ferrari in {}>, <Name u"Ferrari S.p.A" - fullname for topic ferrari in {}>]
You can disable subtype checking by setting subtypes=False.
>>> ferrari.listNames(type=ztm.topicmaps.psis.model.topic_name, subtypes=False)
[<Name u"Ferrari" - topic-name for topic ferrari in {}>]
Subscopes are handled similarly.
>>> ukenglish = topicmap.createTopic(__reprname__='en_GB')
>>> ukenglish.addSubjectIdentifier(u'http://psi.ztmproject.org/locale/en_GB')
'http://psi.ztmproject.org/locale/en_GB'
>>> ukenglish.addSuperType(english)
>>> american = topicmap.createTopic(__reprname__='en_US')
>>> american.addSubjectIdentifier(u'http://psi.ztmproject.org/locale/en_US')
'http://psi.ztmproject.org/locale/en_US'
>>> american.addSuperType(english)
Set some localized names truck:
>>> truck.createName(u'Lorry', scope=ukenglish)
<Name u"Lorry" - topic-name for topic truck in {en_GB}>
>>> truck.createName(u'Truck', scope=american)
<Name u"Truck" - topic-name for topic truck in {en_US}>
>>> truck.createName(u'Truck', scope=english)
<Name u"Truck" - topic-name for topic truck in {english}>
Truck now has four names. Notice the sortorder as described in ztm.topicmaps.interfaces.ITopic.listNames():
>>> pprint(truck.listNames(scope=english))
[<Name u"Truck" - topic-name for topic truck in {english}>,
<Name u"Lorry" - topic-name for topic truck in {en_GB}>,
<Name u"Truck" - topic-name for topic truck in {en_US}>,
<Name u"Truck" - topic-name for topic truck in {}>]
Notice that the name form the unconstrained scope is included. This is because the unconstrained scope is considered a superset of all scopes.
To disable subtype checking you can either set the subtypes parameter to False, or the strict parameter to False if you only wish to restrict scopes.
Topic names have variants. They are used for things like overriding sorting or display or other application specific tasks. Variants are separated from the name by extra scoping topics.
>>> sort_name = topicmap.createTopic(subjectidentifiers=(ztm.topicmaps.psis.model.sort,), __reprname__='sort_name')
>>> variant = name.createVariant('scania', sort_name)
>>> variant in name.variants
True
>>> variant.parent is name
True
>>> description = topicmap.createTopic(__reprname__='description')
>>> description.addSubjectIdentifier(ztm.topicmaps.psis.dc.description)
'http://purl.org/dc/elements/1.1/description'
>>> occ = car.createOccurrence('A car has 4 wheels.', type=ztm.topicmaps.psis.dc.description)
>>> occ.value
u'A car has 4 wheels.'
Scope, types and index work the same way as for names, but there is no default type.
>>> occ.type
<Topic description>
>>> occ.scope
<Scope {}>
Occurrences can hold more than text. There is a somewhat under-developed infrastructure for dealing with them.
#TODO: Use a better data type.
>>> DATA = ztm.topicmaps.datatype.DataType.DATA
>>> settings = topicmap.createTopic(__reprname__='settings') # A new occurrence type
>>> car.createOccurrence({'key':'value'}, type=settings, datatype=DATA)
<Occurrence settings (internal:dictionary) for topic car in {}>
>>> occ = _
>>> occ.value
{'key': 'value'}
>>> type(occ.value)
<type 'dict'>
>>> car.createOccurrence({'key':'othervalue'}, type=settings, datatype=DATA)
<Occurrence settings (internal:dictionary) for topic car in {}>
If you try to create an occurrence whose type, datatype, scope and value already exists for the topic the original occurrence is returned instead of a new merged occurrence.
>>> car.createOccurrence({'key':'value'}, type=settings, datatype=DATA)
<Occurrence settings (internal:dictionary) for topic car in {}>
>>> car.listOccurrences()
[<Occurrence description (http://www.w3.org/2001/XMLSchema#string) for topic car in {}>, <Occurrence settings (internal:dictionary) for topic car in {}>, <Occurrence settings (internal:dictionary) for topic car in {}>]
The standard API for creating associations is lowlevel and somewhat cumbersome because of requirements to support n-ary associations.
Fortunately virtually all associations are binary and there is a simple API for that.
You are supposed to hide these behind higher level APIs.
First we create some association and role types to let us categorize the cars. (There are no default types like there is for names.):
>>> create = topicmap.createTopic # Keep the lines shorter
>>> categorization = create(__reprname__='categorization', subjectidentifiers=u'http://psi.ztmproject.org/categorization')
>>> categorized = create(__reprname__='categorized', subjectidentifiers=u'http://psi.ztmproject.org/categorized')
>>> category = create(__reprname__='category', subjectidentifiers=u'http://psi.ztmproject.org/category')
Then we add some categories:
>>> # Fast and slow are some suitably silly categories
>>> fast = topicmap.createTopic(__reprname__='fast')
>>> slow = topicmap.createTopic(__reprname__='slow')
Now we can create associations:
>>> topicmap.createAssociation(categorization, [(category, fast), (categorized,ferrari)])
<Association categorization(...) in {}>
>>> assoc = _
>>> pprint(assoc.roles)
(<Role where ferrari is categorized in a categorization association with fast in {}>,
<Role where fast is category in a categorization association with ferrari in {}>)
>>> alpharomeo.createBinaryAssociation(fast, categorization, categorized, category)
<Association categorization(categorized:alpharomeo, category:fast) in {}>
>>> fastromeo = _ # storing for later
You can look up roles with the associatedTopicsquery method.
>>> list(fast.associatedTopicsQuery(associationtype=categorization))
[<Topic alpharomeo>, <Topic ferrari>]
>>>
>>> list(fast.associatedTopicsQuery(roletype=category))
[<Topic alpharomeo>, <Topic ferrari>]
>>>
>>> list(ferrari.associatedTopicsQuery(roletype=categorized))
[<Topic fast>]
>>> list(ferrari.associatedTopicsQuery(roletype=category))
[]
Associations must have at least one role. Virtually all associations are binary. The only exception so far are some n-ary associations generated by the ztm.ontology package.
>>> [role.player for role in assoc.roles]
[<Topic ferrari>, <Topic fast>]
>>> createAssociation = topicmap.createAssociation
Only one association of a particular signature of associationtypes, roletypes, scope and players can exist.
>>> createAssociation(categorization, roles=[(category, fast), (categorized, alpharomeo)])
<Association ...>
>>> _ is fastromeo
True
>>> createAssociation(categorization, roles=[(category, slow), (categorized, scania)])
<Association ...>
Associations also have scope.
>>> # Scania trucks are fast in norwegian (yeah, that makes noe sense :-)
>>> createAssociation(categorization, roles=[(category, slow), (categorized, scania)], scope=norwegian)
<Association ...>
>>> assoc = _
>>> assoc.scope.themes
frozenset([<Topic norwegian>])
Roles inherit the scope from the association.
>>> [role.scope.themes for role in assoc.roles]
[frozenset([<Topic norwegian>]), frozenset([<Topic norwegian>])]
If you know all the role- and association-types you’re looking for, this might be faster:
>>> list(ferrari.associatedTopics(associationtype=categorization, roletype=categorized, otherroletype=category))
[<Topic fast>]
All topics playing a role:
>>> list(category.roleplayers)
[<Topic fast>, <Topic slow>]
Roles and associations cannot be updated (except adding and removing identifiers and reifiers). You will have to create a new one.
>>> list(ferrari.listRoles(type=categorized))
[<Role where ferrari is categorized in a categorization association with fast in {}>]
A scope object consists of one or more themes. (The unconstrained scope is a special case with no themes.) and is used to specify the validity of assertions like names, occurrences and associations).
>>> name.scope
<Scope {}>
>>> topicmap.getScope(english)
<Scope {english}>
Scope objects have indexes where you can retrieve items in the scope.
>>> englishscope = _
>>> import operator
>>> pprint(sorted(englishscope.directnameinstances, key=operator.attrgetter('value')))
[<Name u"Truck" - topic-name for topic truck in {english}>]
>>> pprint(sorted(repr(name) for name in englishscope.allnameinstances))
['<Name u"Lorry" - topic-name for topic truck in {en_GB}>',
'<Name u"Truck" - topic-name for topic truck in {en_US}>',
'<Name u"Truck" - topic-name for topic truck in {english}>']
Notice that allnameinstances does noe include names in the unconstrained scope.
There is even a method that can calculate combinations and find all subscopes that are used.
>>> sorted(repr(scope) for scope in englishscope.subscopes())
['<Scope {en_GB}>', '<Scope {en_US}>', '<Scope {english}>']