├── activitystreams ├── extended │ ├── __init__.py │ ├── mention.py │ ├── actor.py │ ├── object.py │ └── activity.py ├── __init__.py └── core.py ├── README.md ├── setup.py └── .gitignore /activitystreams/extended/__init__.py: -------------------------------------------------------------------------------- 1 | from .activity import * 2 | from .actor import * 3 | from .mention import * 4 | from .object import * 5 | -------------------------------------------------------------------------------- /activitystreams/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import Object, Link, Activity, IntransitiveActivity,\ 2 | Collection, CollectionPage, OrderedCollection, OrderedCollectionPage 3 | -------------------------------------------------------------------------------- /activitystreams/extended/mention.py: -------------------------------------------------------------------------------- 1 | from activitystreams.core import Link 2 | 3 | 4 | class Mention(Link): 5 | """A specialized Link that represents an @mention.""" 6 | pass 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | activitystreams-dataclass 2 | ========================= 3 | 4 | Classes for ActivityStreams objects, built using [PEP 557][] dataclasses. 5 | 6 | **WARNING** experiment-in-progress, not very useful as of yet. You may 7 | want to try [activipy](https://activipy.readthedocs.io/en/latest/) instead. 8 | 9 | [PEP 557]: https://www.python.org/dev/peps/pep-0557/ 10 | -------------------------------------------------------------------------------- /activitystreams/extended/actor.py: -------------------------------------------------------------------------------- 1 | from activitystreams.core import Object 2 | 3 | 4 | class Actor(Object): 5 | """Describes a generic actor.""" 6 | pass 7 | 8 | 9 | class Application(Actor): 10 | """Describes a software application.""" 11 | pass 12 | 13 | 14 | class Group(Actor): 15 | """Represents a formal or informal collective of Actors.""" 16 | pass 17 | 18 | 19 | class Organization(Actor): 20 | """Represents an organization.""" 21 | pass 22 | 23 | 24 | class Person(Actor): 25 | """Represents an individual person.""" 26 | pass 27 | 28 | 29 | class Service(Actor): 30 | """Represents a service of any kind.""" 31 | pass 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="activitystreams-dataclass", 8 | version="0.0.2", 9 | author="Ben Jeffrey", 10 | author_email="mail@benjeffrey.net", 11 | description="Dataclasses for ActivityStreams objects", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/jeffbr13/activitystreams-dataclass", 15 | packages=setuptools.find_packages(), 16 | classifiers=( 17 | "Programming Language :: Python :: 3.7", 18 | "Operating System :: OS Independent", 19 | ), 20 | ) 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | -------------------------------------------------------------------------------- /activitystreams/extended/object.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from typing import Optional 4 | 5 | from activitystreams.core import Object, ActivityStreamsEntity 6 | 7 | 8 | @dataclass 9 | class Relationship(Object): 10 | """Describes a relationship between two individuals. 11 | 12 | The subject and object properties are used to identify the connected individuals. 13 | See 5.2 Representing Relationships Between Entities for additional information. 14 | """ 15 | subject: ActivityStreamsEntity = None 16 | object: ActivityStreamsEntity = None 17 | relationship: str = None 18 | 19 | 20 | @dataclass 21 | class Place(Object): 22 | """Represents a logical or physical location. 23 | 24 | If units is not specified, the default is assumed to be "m" indicating meters. 25 | """ 26 | accuracy: Optional[float] = None 27 | altitude: Optional[float] = None 28 | latitude: Optional[float] = None 29 | longitude: Optional[float] = None 30 | radius: Optional[float] = None 31 | units: Optional[str] = None 32 | 33 | 34 | class Article(Object): 35 | """Represents any kind of multi-paragraph written work.""" 36 | pass 37 | 38 | 39 | class Note(Object): 40 | """Represents a short written work typically less than a single paragraph in length. 41 | """ 42 | pass 43 | 44 | 45 | class Document(Object): 46 | """Represents a document of any kind.""" 47 | pass 48 | 49 | 50 | class Audio(Document): 51 | """Represents an audio document of any kind.""" 52 | pass 53 | 54 | 55 | class Image(Document): 56 | """An image document of any kind.""" 57 | pass 58 | 59 | 60 | class Video(Document): 61 | """A video document of any kind.""" 62 | pass 63 | 64 | 65 | class Page(Document): 66 | """Represents a Web Page.""" 67 | pass 68 | 69 | 70 | class Event(Object): 71 | """Represents any kind of event.""" 72 | pass 73 | 74 | 75 | @dataclass 76 | class Profile(Object): 77 | """A Profile is a content object that describes another Object. 78 | 79 | Typically used to describe Actor Type objects. 80 | The describes property is used to reference the object being described by the profile. 81 | """ 82 | describes: Object = None 83 | 84 | 85 | class Tombstone(Object): 86 | """A Tombstone represents a content object that has been deleted. 87 | 88 | It can be used in Collections to signify that there used to be an object at this position, 89 | but it has been deleted. 90 | """ 91 | formerType: Optional[str] = None 92 | deleted: Optional[datetime] = None 93 | -------------------------------------------------------------------------------- /activitystreams/extended/activity.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from typing import Optional, Collection, Union 4 | 5 | from activitystreams.core import Activity, IntransitiveActivity, ActivityStreamsEntity 6 | 7 | 8 | class Accept(Activity): 9 | """Indicates that the actor accepts the object. 10 | 11 | The target property can be used in certain circumstances 12 | to indicate the context into which the object has been accepted. 13 | """ 14 | pass 15 | 16 | 17 | class TentativeAccept(Accept): 18 | """A specialization of Accept indicating that the acceptance is tentative. 19 | """ 20 | pass 21 | 22 | 23 | class Add(Activity): 24 | """Indicates that the actor has added the object to the target. 25 | 26 | If the target property is not explicitly specified, the target would need to be determined implicitly by context. 27 | The origin can be used to identify the context from which the object originated. 28 | """ 29 | pass 30 | 31 | 32 | class Arrive(IntransitiveActivity): 33 | """An IntransitiveActivity that indicates that the actor has arrived at the location. 34 | The origin can be used to identify the context from which the actor originated. 35 | The target typically has no defined meaning. 36 | """ 37 | pass 38 | 39 | 40 | class Announce(Activity): 41 | """Indicates that the actor is calling the target's attention the object. 42 | 43 | The origin typically has no defined meaning. 44 | """ 45 | pass 46 | 47 | 48 | class Create(Activity): 49 | """Indicates that the actor has created the object. 50 | """ 51 | pass 52 | 53 | 54 | class Delete(Activity): 55 | """Indicates that the actor has deleted the object. 56 | 57 | If specified, the origin indicates the context from which the object was deleted. 58 | """ 59 | pass 60 | 61 | 62 | class Follow(Activity): 63 | """Indicates that the actor is "following" the object. 64 | Following is defined in the sense typically used within Social systems 65 | in which the actor is interested in any activity performed by or on the object. 66 | The target and origin typically have no defined meaning. 67 | """ 68 | pass 69 | 70 | 71 | class Ignore(Activity): 72 | """Indicates that the actor is ignoring the object. 73 | 74 | The target and origin typically have no defined meaning. 75 | """ 76 | pass 77 | 78 | 79 | class Block(Ignore): 80 | """Indicates that the actor is blocking the object. 81 | 82 | Blocking is a stronger form of Ignore. 83 | The typical use is to support social systems that allow one user to block activities or content of other users. 84 | The target and origin typically have no defined meaning. 85 | """ 86 | pass 87 | 88 | 89 | class Join(Activity): 90 | """Indicates that the actor has joined the object. 91 | 92 | The target and origin typically have no defined meaning. 93 | """ 94 | pass 95 | 96 | 97 | class Leave(Activity): 98 | """Indicates that the actor has left the object. 99 | 100 | The target and origin typically have no meaning. 101 | """ 102 | pass 103 | 104 | 105 | class Like(Activity): 106 | """Indicates that the actor likes, recommends or endorses the object. 107 | 108 | The target and origin typically have no defined meaning. 109 | """ 110 | pass 111 | 112 | 113 | class Dislike(Activity): 114 | """Indicates that the actor dislikes the object.""" 115 | pass 116 | 117 | 118 | class Flag(Activity): 119 | """Indicates that the actor is "flagging" the object. 120 | 121 | Flagging is defined in the sense common to many social platforms 122 | as reporting content as being inappropriate for any number of reasons. 123 | """ 124 | pass 125 | 126 | 127 | class Offer(Activity): 128 | """Indicates that the actor is offering the object. 129 | 130 | If specified, the target indicates the entity to which the object is being offered. 131 | """ 132 | pass 133 | 134 | 135 | class Invite(Offer): 136 | """A specialization of Offer in which the actor is extending an invitation for the object to the target. 137 | """ 138 | pass 139 | 140 | 141 | @dataclass 142 | class Question(IntransitiveActivity): 143 | """Represents a question being asked. 144 | 145 | Question objects are an extension of IntransitiveActivity. 146 | That is, the Question object is an Activity, but the direct object 147 | is the question itself and therefore it would not contain an object property. 148 | 149 | Either of the anyOf and oneOf properties may be used to express possible answers, 150 | but a Question object must not have both properties. 151 | """ 152 | oneOf: Collection[ActivityStreamsEntity] = None 153 | anyOf: Collection[ActivityStreamsEntity] = None 154 | closed: Optional[Union[ActivityStreamsEntity, datetime, bool]] = None 155 | 156 | def __post_init__(self): 157 | super().__post_init__() 158 | assert not (self.oneOf and self.anyOf), "'anyOf' and 'oneOf' are mutually exclusive" 159 | 160 | 161 | class Reject(Activity): 162 | """Indicates that the actor is rejecting the object. 163 | 164 | The target and origin typically have no defined meaning. 165 | """ 166 | pass 167 | 168 | 169 | class TentativeReject(Reject): 170 | """A specialization of Reject in which the rejection is considered tentative. 171 | """ 172 | pass 173 | 174 | 175 | class Remove(Activity): 176 | """Indicates that the actor is removing the object. 177 | 178 | If specified, the origin indicates the context from which the object is being removed. 179 | """ 180 | pass 181 | 182 | 183 | class Undo(Activity): 184 | """Indicates that the actor is undoing the object. 185 | 186 | In most cases, the object will be an Activity describing some previously performed action. 187 | 188 | The target and origin typically have no defined meaning. 189 | """ 190 | pass 191 | 192 | 193 | class Update(Activity): 194 | """Indicates that the actor has updated the object. 195 | 196 | Note, however, that this vocabulary does not define a mechanism for describing the actual set of modifications made to object. 197 | 198 | The target and origin typically have no defined meaning. 199 | """ 200 | pass 201 | 202 | 203 | class View(Activity): 204 | """Indicates that the actor has viewed the object. 205 | """ 206 | pass 207 | 208 | 209 | class Listen(Activity): 210 | """Indicates that the actor has listened to the object.""" 211 | pass 212 | 213 | 214 | class Read(Activity): 215 | """Indicates that the actor has read the object.""" 216 | pass 217 | 218 | 219 | class Move(Activity): 220 | """Indicates that the actor has moved object from origin to target. 221 | 222 | If the origin or target are not specified, either can be determined by context. 223 | """ 224 | pass 225 | 226 | 227 | class Travel(IntransitiveActivity): 228 | """Indicates that the actor is traveling to target from origin. 229 | 230 | Travel is an IntransitiveObject whose actor specifies the direct object. 231 | If the target or origin are not specified, either can be determined by context. 232 | """ 233 | pass 234 | -------------------------------------------------------------------------------- /activitystreams/core.py: -------------------------------------------------------------------------------- 1 | import json 2 | from dataclasses import dataclass 3 | from datetime import datetime, timedelta 4 | from typing import Union, Optional, Mapping, Collection as CollectionType 5 | 6 | 7 | LD_NAMESPACE = 'https://www.w3.org/ns/activitystreams' 8 | 9 | LangString = Mapping[str, str] # todo - limit keys to IETF language tags 10 | String = Union[str, LangString] 11 | 12 | ActivityStreamsImageEntity = Union['ActivityStreamsImage', 'ActivityStreamsLink'] 13 | 14 | ActivityStreamsCollectionEntity = Union['ActivityStreamsCollection', 'ActivityStreamsLink'] 15 | ActivityStreamsCollectionPageEntity = Union['ActivityStreamsCollectionPage', 'ActivityStreamsLink'] 16 | ActivityStreamsOrderedCollectionEntity = Union['ActivityStreamsOrderedCollection', 'ActivityStreamsLink'] 17 | ActivityStreamsOrderedCollectionPageEntity = Union['ActivityStreamsOrderedCollectionPage', 'ActivityStreamsLink'] 18 | 19 | 20 | class ActivityStreamsEntity: 21 | def __repr__(self): 22 | return json.dumps(self.to_dict()) 23 | 24 | def to_dict(self): 25 | raise NotImplementedError 26 | 27 | 28 | @dataclass 29 | class Object(ActivityStreamsEntity): 30 | name: String = None 31 | content: String = None 32 | summary: Optional[String] = None 33 | published: Optional[datetime] = None 34 | attachment: Optional[ActivityStreamsEntity] = None 35 | updated: Optional[datetime] = None 36 | attributedTo: Optional[ActivityStreamsEntity] = None 37 | audience: CollectionType[ActivityStreamsEntity] = None 38 | context: Optional[ActivityStreamsEntity] = None 39 | startTime: Optional[datetime] = None 40 | endTime: Optional[datetime] = None 41 | duration: Optional[timedelta] = None 42 | generator: Optional[ActivityStreamsEntity] = None 43 | mediaType: Optional[str] = None 44 | icon: Optional[ActivityStreamsImageEntity] = None 45 | image: Optional[ActivityStreamsImageEntity] = None 46 | url: Optional['Link'] = None 47 | inReplyTo: CollectionType[ActivityStreamsEntity] = None 48 | replies: Optional['Collection'] = None 49 | location: CollectionType[ActivityStreamsEntity] = None 50 | preview: CollectionType[ActivityStreamsEntity] = None 51 | tag: CollectionType[ActivityStreamsEntity] = None 52 | to: CollectionType[ActivityStreamsEntity] = None 53 | bto: CollectionType[ActivityStreamsEntity] = None 54 | cc: CollectionType[ActivityStreamsEntity] = None 55 | bcc: CollectionType[ActivityStreamsEntity] = None 56 | 57 | @property 58 | def type(self): 59 | return f'{LD_NAMESPACE}#{self.__class__.__name__}' 60 | 61 | def to_dict(self): 62 | return { 63 | '@context': LD_NAMESPACE, 64 | 'type': self.type, 65 | # data 66 | 'name': self.name, 67 | 'content': self.content, 68 | 'attachment': self.attachment, 69 | 'attributedTo': self.attributedTo, 70 | 'audience': self.audience, 71 | 'context': self.context, 72 | 'endTime': self.endTime, 73 | 'generator': self.generator, 74 | 'icon': self.icon, 75 | 'image': self.image, 76 | 'inReplyTo': self.inReplyTo, 77 | 'location': self.location, 78 | 'preview': self.preview, 79 | 'published': self.published, 80 | 'replies': self.replies, 81 | 'startTime': self.startTime, 82 | 'summary': self.summary, 83 | 'tag': self.tag, 84 | 'updated': self.updated, 85 | 'url': self.url, 86 | 'to': self.to, 87 | 'bto': self.bto, 88 | 'cc': self.cc, 89 | 'bcc': self.bcc, 90 | 'mediaType': self.mediaType, 91 | 'duration': self.duration, 92 | } 93 | 94 | 95 | @dataclass 96 | class Link(ActivityStreamsEntity): 97 | href: str 98 | rel: Optional[str] = None 99 | mediaType: Optional[str] = None 100 | name: Optional[String] = None 101 | hreflang: Optional[str] = None 102 | height: Optional[int] = None 103 | width: Optional[int] = None 104 | preview: Optional[ActivityStreamsEntity] = None 105 | 106 | @property 107 | def type(self): 108 | return f'{LD_NAMESPACE}#{self.__class__.__name__}' 109 | 110 | def to_dict(self): 111 | return { 112 | '@context': LD_NAMESPACE, 113 | 'type': self.type, 114 | 'href': self.href, 115 | 'rel': self.rel, 116 | 'mediaType': self.mediaType, 117 | 'name': self.name, 118 | 'hreflang': self.hreflang, 119 | 'height': self.height, 120 | 'width': self.width, 121 | 'preview': self.preview, 122 | } 123 | 124 | 125 | @dataclass 126 | class Activity(Object): 127 | actor: ActivityStreamsEntity = None 128 | object: Object = None 129 | target: CollectionType[ActivityStreamsEntity] = None 130 | result: Optional[ActivityStreamsEntity] = None 131 | origin: Optional[ActivityStreamsEntity] = None 132 | instrument: Optional[ActivityStreamsEntity] = None 133 | 134 | def to_dict(self): 135 | d = super().to_dict() 136 | d.update({ 137 | 'actor': self.actor, 138 | 'object': self.object, 139 | 'target': self.target, 140 | 'result': self.result, 141 | 'origin': self.origin, 142 | 'instrument': self.instrument, 143 | }) 144 | return d 145 | 146 | 147 | @dataclass 148 | class IntransitiveActivity(Object): 149 | actor: ActivityStreamsEntity = None 150 | target: CollectionType[ActivityStreamsEntity] = None 151 | result: Optional[ActivityStreamsEntity] = None 152 | origin: Optional[ActivityStreamsEntity] = None 153 | instrument: Optional[ActivityStreamsEntity] = None 154 | 155 | def to_dict(self): 156 | d = super().to_dict() 157 | d.update({ 158 | 'actor': self.actor, 159 | 'target': self.target, 160 | 'result': self.result, 161 | 'origin': self.origin, 162 | 'instrument': self.instrument, 163 | }) 164 | return d 165 | 166 | 167 | @dataclass 168 | class Collection(Object): 169 | totalItems: Optional[int] = None 170 | current: Optional[ActivityStreamsCollectionPageEntity] = None 171 | first: Optional[ActivityStreamsCollectionPageEntity] = None 172 | last: Optional[ActivityStreamsCollectionPageEntity] = None 173 | items: CollectionType[ActivityStreamsEntity] = None 174 | 175 | def __post_init__(self): 176 | assert self.items or (self.current or self.first or self.last), \ 177 | "Collection must have either 'items' or first/current/last CollectionPages" 178 | 179 | def to_dict(self): 180 | d = super().to_dict() 181 | d.update({ 182 | 'totalItems': self.totalItems, 183 | 'current': self.current, 184 | 'first': self.first, 185 | 'last': self.last, 186 | 'items': self.items, 187 | }) 188 | return d 189 | 190 | 191 | @dataclass 192 | class OrderedCollection(Collection): 193 | current: Optional[ActivityStreamsOrderedCollectionPageEntity] = None 194 | first: Optional[ActivityStreamsOrderedCollectionPageEntity] = None 195 | last: Optional[ActivityStreamsOrderedCollectionPageEntity] = None 196 | 197 | 198 | @dataclass 199 | class CollectionPage(Collection): 200 | partOf: Optional[ActivityStreamsCollectionEntity] = None 201 | next: Optional[ActivityStreamsCollectionPageEntity] = None 202 | prev: Optional[ActivityStreamsCollectionPageEntity] = None 203 | 204 | def to_dict(self): 205 | d = super().to_dict() 206 | d.update({ 207 | 'partOf': self.partOf, 208 | 'next': self.next, 209 | 'prev': self.prev, 210 | }) 211 | return d 212 | 213 | 214 | @dataclass 215 | class OrderedCollectionPage(OrderedCollection): 216 | partOf: Optional[ActivityStreamsOrderedCollectionEntity] = None 217 | next: Optional[ActivityStreamsOrderedCollectionPageEntity] = None 218 | prev: Optional[ActivityStreamsOrderedCollectionPageEntity] = None 219 | 220 | def to_dict(self): 221 | d = super().to_dict() 222 | d.update({ 223 | 'partOf': self.partOf, 224 | 'next': self.next, 225 | 'prev': self.prev, 226 | }) 227 | return d 228 | --------------------------------------------------------------------------------