Just a quick heads-up: all parcels' schemas are now defined using the
Python schema API. I'll be working next on cleaning up the migration tools
and checking them in so that we'll have them available to help third-party
parcel developers, but also because we may later want a way to generate
Python schema from data in the repository (e.g. if we want to create a
kind->class wizard or something like that, or if we want to encode the
repository core schema as Python code.)
After that, I'll be working on producing better documentation for the
schema API. In the meantime, here's a quick refresher on the main concepts:
Base Classes
------------
The primary schema base classes are Item, Struct, and Enumeration, used as
follows::
from application import schema
class Mood(schema.Enumeration):
schema.kindInfo(
displayName = "Mood",
description = "User's Possible Moods",
)
values = "mad", "sad", "glad"
class SpaceTimeCoordinates(schema.Struct):
__slots__ = "x", "y", "z", "t"
schema.kindInfo(
displayName = "Space/Time Co-ordinates",
description = "The user's current location in the space/time "
"continuum.",
)
class MyNewKind(schema.Item):
schema.kindInfo(
# kindInfo is optional, and you don't have to set all of these
displayName = "My New Kind",
description = "This is just an example",
displayAttribute = "attributeToUseForDisplay",
)
attributeToUseForDisplay = schema.One(schema.String)
whereYouAre = schema.One(
SpaceTimeCoordinates,
initialValue=SpaceTimeCoordinates(0,0,0,0)
)
howYouFeel = schema.One(Mood, initialValue="glad")
whereYouveBeen = schema.Sequence(
SpaceTimeCoordinates, initialValue=[]
)
schema.addClouds(
default = schema.Cloud(byValue=[whereYouAre,howYouFeel]),
sharing = schema.Cloud(byCloud=[whereYouveBeen]),
)
Attribute Definitions
---------------------
There are four kinds of attribute definitions: One, Many, Sequence, and
Mapping, which correspond to the repository cardinalities of single, set,
list, and dict, respectively. The first argument to an attribute
definition is its type, which may be one of the following things:
* An enumeration class (schema.Enumeration subclass)
* A struct class (schema.Struct subclass)
* A kind class (schema.Item or a subclass thereof)
* A string telling where to import one of the above
* An API-provided shortcut for a core schema type reference (schema.String,
schema.Integer, etc.)
* An explicit core schema type reference, e.g.
``schema.TypeReference("//Schema/Core/Kind")``
The current API-provided type shortcuts are Boolean, String, Integer, Long,
Float, Tuple, List, Set, Class, Dictionary, Anything, Date, Time, DateTime,
TimeDelta, Lob, Symbol, URL, Complex, UUID, Path, and SingleRef. These are
not automatically created, but have to be added to schema.py manually to
avoid naming conflict with other names exposed by the schema API. For
example, ``schema.Cloud`` cannot be used as an attribute type, because it
is an API for creating cloud definition templates. To create an attribute
of type "cloud", you must use
``schema.TypeReference("//Schema/Core/Cloud")`` as the attribute type.
Strings can be used to refer to types that have not yet been defined. If
the type being referenced is in the same module (or is the same class), you
can use just the class name, e.g.::
class File(schema.Item):
container = schema.One("Folder", inverse="contents")
class Folder(File):
contents = schema.Many(File, inverse=File.container)
Because ``Folder`` doesn't exist when the ``File.container`` attribute is
defined, it must use a string to refer ahead, for both the type and the
inverse attribute setting. But when ``Folder`` is defined, ``File``
already exists, so it can refer to it and the container attribute directly.
If ``Folder`` had been in a different module, however, the type string
would've needed to name that module, either as a relative name (e.g.
"Folders.Folder") or absolute (e.g.
"osaf.somewhere.something.Folders.Folder"). The relative form only works
for modules that are peers (i.e., that are both directly contained in the
same package.)
In general, you should use the ``inverse`` keyword rather than
``otherName`` to set an attribute's inverse, because the schema API can
perform more error checking that way. (E.g. it ensures that both sides of
a bidirectional relationship in fact point to each other). However, there
are sometimes unusual schema elements where the reference isn't really
bidirectional on a type-to-type basis. That is, there are three or more
classes participating in the relationship. In these cases you will
probably need to use ``otherName``, as you will get schema API errors when
you try to add a third attribute to a relationship.
The remaining keyword arguments used to define attributes are the same as
normal repository attribute aspects, e.g. initialValue, defaultValue,
description, examples, issues, displayName, required, persist, redirectTo,
etc. When setting ``initialValue``, you should use the appropriate Python
expression to create the value, which may not be an exact match of the
string(s) you would have used in parcel.xml.
Kind Info and Clouds
--------------------
As you've seen in the examples above, the ``schema.kindInfo()`` API lets
you set attribute values on the Kind, Struct, or Enumeration that the
schema API generates from your classes.
In addition, the ``schema.addClouds()`` API lets you define your kind's
clouds in a very compact manner. It takes a series of keyword arguments,
each of which defines a cloud alias. So ``schema.addClouds(foo = expr)``
creates a cloud alias ``foo`` that maps to the value of ``expr``.
Ordinarily, the expressions you'll use will be ``schema.Cloud``
instances. ``schema.Cloud`` accepts zero or more "by value" endpoints, and
keyword arguments mapping inclusion policies to endpoint groups. So this::
schema.addClouds(
sharing = schema.Cloud(emailAddress, byCloud=[contactName])
)
Creates a cloud named "SharingCloud", with a "byValue" endpoint for
emailAddress, and a "byCloud" endpoint for contactName.
Typically, you'll be defining endpoints by using the attribute objects you
just defined for your class, so you don't normally need to put quotes
around them. If you're defining something for an inherited attribute, you
may need to use the superclass name, e.g.::
schema.addClouds(
sharing = schema.Cloud(byCloud = [ContentItem.creator])
)
However, if the attribute is part of the core schema, you may have to use
quotes::
schema.addClouds(
sharing = schema.Cloud( none = ["displayName"] )
)
Notice, by the way, that using a policy of "none" (in all lowercase) is how
you can remove a superclass endpoint from a subclass' cloud.
Finally, if you have special needs for an endpoint that can't be met using
any of these syntaxes, you can list ``schema.Endpoint`` instances in your
groups, in order to do things like manually set the endpoint's
``cloudAlias`` or ``method`` attributes.
Parcel Paths
------------
Currently, all kinds have the same repository paths as they did before the
migration. The parcel that kinds are placed in is determined by a
``__parcel__`` constant set at the top of the module. If ``__parcel__`` is
not set, the API will consider that module to be a parcel unto itself. So
for example, a module ``osaf.framework.blocks.Block`` would be considered
the parcel ``//parcels/osaf/framework/blocks/Block``, if it didn't have a
``__parcel__`` setting. But currently, it has a ``__parcel__`` of
``osaf.framework.blocks``, so all its contained classes live in the
``//parcels/osaf/framework/blocks`` parcel.
You need to be aware of this if you care where your kind(s) live, or if you
need to determine their paths, especially if you are moving them or
creating new ones.
Another important thing to note is that if a ``__parcel__`` setting is
used, you *must* add import statements to the named package, to import all
of the classes defined in the module that used ``__parcel__``. For
example, because ``osaf.framework.blocks.Block`` sets ``__parcel__`` to
``osaf.framework.blocks``, the __init__.py for ``osaf.framework.blocks``
has to import every kind class from the ``Block`` module. This is so that
the parcel loader can ensure that it knows about the whole schema before it
tries to load any parcels that depend on ``osaf.framework.blocks``.
So, if you add a new persistent class to a module, please check for a
``__parcel__`` setting, and if one is present, add or update the import
statement in the corresponding module or package.
(Note that in the long term, we want to phase out all use of
``__parcel__``, because it was introduced solely as a way to leave our
existing repository paths alone and thus minimize disruption while
migrating to the new API.)
Custom Parcel Classes
---------------------
You can set a parcel's class by setting a __parcel_class__ variable in the
module that corresponds to that parcel.
Whew. I think that covers most of the ground. You can also find more
detailed documentation on some of these issues in the ``schema_api.txt``
file, located in the ``application`` package alongside the ``schema`` module.
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
Open Source Applications Foundation "Dev" mailing list
http://lists.osafoundation.org/mailman/listinfo/dev