├── .gitignore
├── .travis.yml
├── LICENSE
├── NOTICE
├── README.md
├── docs
├── Makefile
├── build
│ └── .empty
└── source
│ ├── conf.py
│ └── index.rst
├── examples
├── __init__.py
├── example_script.py
└── resources
│ ├── __init__.py
│ └── lrs_properties.py.template
├── setup.cfg
├── setup.py
├── test
├── __init__.py
├── about_test.py
├── activity_test.py
├── activitydefinition_test.py
├── activitylist_test.py
├── agent_test.py
├── attachment_test.py
├── context_test.py
├── contextactivities_test.py
├── conversions
│ ├── __init__.py
│ └── iso8601_test.py
├── documents
│ ├── __init__.py
│ ├── activity_profile_document_test.py
│ ├── agent_profile_document_test.py
│ ├── document_test.py
│ └── state_document_test.py
├── extensions_test.py
├── group_test.py
├── http_request_test.py
├── interactioncomponent_test.py
├── interactioncomponentlist_test.py
├── languagemap_test.py
├── lrs_response_test.py
├── main.py
├── remote_lrs_test.py
├── resources
│ ├── __init__.py
│ ├── lrs_properties.py.template
│ └── lrs_properties.py.travis-ci
├── result_test.py
├── score_test.py
├── statement_test.py
├── statementref_test.py
├── statements_result_test.py
├── substatement_test.py
├── template_test.py.template
├── test_utils.py
├── typedlist_test.py
├── verb_test.py
└── version_test.py
└── tincan
├── __init__.py
├── about.py
├── activity.py
├── activity_definition.py
├── activity_list.py
├── agent.py
├── agent_account.py
├── agent_list.py
├── attachment.py
├── attachment_list.py
├── base.py
├── context.py
├── context_activities.py
├── conversions
├── __init__.py
└── iso8601.py
├── documents
├── __init__.py
├── activity_profile_document.py
├── agent_profile_document.py
├── document.py
└── state_document.py
├── extensions.py
├── group.py
├── http_request.py
├── interaction_component.py
├── interaction_component_list.py
├── language_map.py
├── lrs_response.py
├── remote_lrs.py
├── result.py
├── score.py
├── serializable_base.py
├── statement.py
├── statement_base.py
├── statement_list.py
├── statement_ref.py
├── statement_targetable.py
├── statements_result.py
├── substatement.py
├── typed_list.py
├── verb.py
└── version.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Third party libraries
2 |
3 | # buildables
4 | *.pyc
5 | docs/build
6 | docs/source/modules.rst
7 | docs/source/tincan.rst
8 | docs/source/tincan.conversions.rst
9 | docs/source/tincan.documents.rst
10 | test/resources/lrs_properties.py
11 |
12 | # bad
13 | .DS_Store
14 | *.swp
15 | tmp/
16 | .idea/
17 | test/resources/lrs_properties.py
18 |
19 | # release
20 | dist
21 | tincan.egg-info/
22 |
23 | # wild
24 | todo.md
25 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "3.8"
4 | - "3.7"
5 | - "3.6"
6 | install: pip3 install aniso8601 pytz
7 | before_script:
8 | - cd test
9 | - cp resources/lrs_properties.py.travis-ci resources/lrs_properties.py
10 | script: python3 main.py
11 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | TinCanPython
2 | Copyright 2014 Rustici Software
3 |
4 | This product includes software developed at
5 | Rustici Software (http://www.rusticisoftware.com/).
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | A Python library for implementing Tin Can API.
2 |
3 | [](https://travis-ci.org/RusticiSoftware/TinCanPython)
4 |
5 | For hosted API documentation, basic usage instructions, supported version listing, etc. visit the main project website at:
6 |
7 |
8 |
9 | For more information about the Tin Can API visit:
10 |
11 |
12 |
13 | Requires Python 3.6 or later.
14 |
15 | ## Installation
16 | TinCanPython requires [Python 3.6](https://www.python.org/downloads/) or later.
17 |
18 | If you are installing from the Github repo, you will need to install `aniso8601` and `pytz` (use `sudo` as necessary):
19 |
20 | pip3 install aniso8601 pytz
21 |
22 | ## Testing
23 | The preferred way to run the tests is from the command line.
24 |
25 | ### Unix-like systems and Mac OS X
26 | No preparation needed.
27 |
28 | ### Windows
29 | Make sure that your Python installation allows you to run `python` from the command line. If not:
30 |
31 | 1. Run the Python installer again.
32 | 2. Choose "Change Python" from the list.
33 | 3. Include "Add python.exe to Path" in the install options. I.e. install everything.
34 | 4. Click "Next," then "Finish."
35 |
36 | ### Running the tests
37 | It is possible to run all the tests in one go, or just run one part of the tests to verify a single part of TinCanPython. The tests are located in `test/`.
38 |
39 | #### All the tests:
40 | 1. `cd` to the `test` directory.
41 | 2. Run
42 |
43 | python3 main.py
44 |
45 | #### One of the tests:
46 | 1. `cd` to the root directory.
47 | 2. Run
48 |
49 | python3 -m unittest test.remote_lrs_test
50 | Where "remote_lrs_test.py" is the test file you want to run
51 |
52 |
53 | #### A single test case of one of the tests:
54 | 1. `cd` to the root directory.
55 | 2. Run
56 |
57 | python3 -m unittest test.remote_lrs_test.RemoteLRSTest.test_save_statements
58 |
59 | Where "remote_lrs_test" is the test file, "RemoteLRSTest" is the class in that file, and "test_save_statements" is the specific test case.
60 |
61 | ## API doc generation
62 | To automatically generate documentation, at the root of the repository run,
63 |
64 | sphinx-apidoc -f -o ./docs/source/ tincan/
65 |
66 | Then from the `docs/` directory run,
67 |
68 | make html
69 |
70 | The docs will be output to `docs/build/html/`.
71 |
72 | If you would like to change the names of each section, you can do so by modifying `docs/source/tincan.rst`.
73 |
74 | ## Releasing
75 | To release to PyPI, first make sure that you have a PyPI account set up at https://pypi.python.org/pypi (and at
76 | https://testpypi.python.org/pypi if you plan on using the test index). You will also need a `.pypirc` file in your
77 | home directory with the following contents.
78 |
79 | [distutils]
80 |
81 | index-servers =
82 | pypi
83 | pypitest
84 |
85 | [pypi] # authentication details for live PyPI
86 | repository: https://pypi.python.org/pypi
87 | username:
88 | password:
89 |
90 | [pypitest] # authentication details for test PyPI
91 | repository: https://testpypi.python.org/pypi
92 | username:
93 | password:
94 |
95 | The pypitest contents of the `.pypirc` file are optional and are used for hosting to the test PyPI index.
96 |
97 | Update setup.py to contain the correct release version and any other new information.
98 |
99 | To test the register/upload, run the following commands in the repo directory:
100 |
101 | python3 setup.py register -r pypitest
102 | python3 setup.py sdist upload -r pypitest
103 |
104 | You should get no errors and should be able to find this tincan version at https://testpypi.python.org/pypi.
105 |
106 | To register/upload to the live PyPI server, run the following commands in the repo directory:
107 |
108 | python3 setup.py register -r pypi
109 | python3 setup.py sdist upload -r pypi
110 |
111 | The new module should be now be installable with pip.
112 |
113 | pip3 install tincan
114 |
115 | Use sudo as necessary.
116 |
--------------------------------------------------------------------------------
/docs/build/.empty:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RusticiSoftware/TinCanPython/bbc3f9dd5d8385e7b66c693e7f8262561392be74/docs/build/.empty
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. Tin Can Python documentation master file, created by
2 | sphinx-quickstart on Tue Jun 10 12:52:27 2014.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to Tin Can Python's documentation!
7 | ==========================================
8 |
9 | Contents:
10 |
11 | .. toctree::
12 | :maxdepth: 4
13 |
14 | modules
15 |
16 | Indices and tables
17 | ==================
18 |
19 | * :ref:`genindex`
20 | * :ref:`modindex`
21 | * :ref:`search`
22 |
23 |
--------------------------------------------------------------------------------
/examples/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RusticiSoftware/TinCanPython/bbc3f9dd5d8385e7b66c693e7f8262561392be74/examples/__init__.py
--------------------------------------------------------------------------------
/examples/example_script.py:
--------------------------------------------------------------------------------
1 | # An example script showing the functionality of the TinCanPython Library
2 |
3 | import uuid
4 |
5 | from test.resources import lrs_properties
6 | from tincan import (
7 | RemoteLRS,
8 | Statement,
9 | Agent,
10 | Verb,
11 | Activity,
12 | Context,
13 | LanguageMap,
14 | ActivityDefinition,
15 | StateDocument,
16 | )
17 |
18 |
19 | # construct an LRS
20 | print("constructing the LRS...")
21 | lrs = RemoteLRS(
22 | version=lrs_properties.version,
23 | endpoint=lrs_properties.endpoint,
24 | username=lrs_properties.username,
25 | password=lrs_properties.password,
26 | )
27 | print("...done")
28 |
29 | # construct the actor of the statement
30 | print("constructing the Actor...")
31 | actor = Agent(
32 | name='UserMan',
33 | mbox='mailto:tincanpython@tincanapi.com',
34 | )
35 | print("...done")
36 |
37 | # construct the verb of the statement
38 | print("constructing the Verb...")
39 | verb = Verb(
40 | id='http://adlnet.gov/expapi/verbs/experienced',
41 | display=LanguageMap({'en-US': 'experienced'}),
42 | )
43 | print("...done")
44 |
45 | # construct the object of the statement
46 | print("constructing the Object...")
47 | object = Activity(
48 | id='http://tincanapi.com/TinCanPython/Example/0',
49 | definition=ActivityDefinition(
50 | name=LanguageMap({'en-US': 'TinCanPython Library'}),
51 | description=LanguageMap({'en-US': 'Use of, or interaction with, the TinCanPython Library'}),
52 | ),
53 | )
54 | print("...done")
55 |
56 | # construct a context for the statement
57 | print("constructing the Context...")
58 | context = Context(
59 | registration=uuid.uuid4(),
60 | instructor=Agent(
61 | name='Lord TinCan',
62 | mbox='mailto:lordtincan@tincanapi.com',
63 | ),
64 | # language='en-US',
65 | )
66 | print("...done")
67 |
68 | # construct the actual statement
69 | print("constructing the Statement...")
70 | statement = Statement(
71 | actor=actor,
72 | verb=verb,
73 | object=object,
74 | context=context,
75 | )
76 | print("...done")
77 |
78 | # save our statement to the remote_lrs and store the response in 'response'
79 | print("saving the Statement...")
80 | response = lrs.save_statement(statement)
81 |
82 | if not response:
83 | raise ValueError("statement failed to save")
84 | print("...done")
85 |
86 | # retrieve our statement from the remote_lrs using the id returned in the response
87 | print("Now, retrieving statement...")
88 | response = lrs.retrieve_statement(response.content.id)
89 |
90 | if not response.success:
91 | raise ValueError("statement could not be retrieved")
92 | print("...done")
93 |
94 | print("constructing new Statement from retrieved statement data...")
95 | ret_statement = response.content
96 | print("...done")
97 |
98 | # now, using our old statement and our returned statement, we can send multiple statements
99 | # note: these statements are logically identical, but are 2 separate objects
100 | print("saving both Statements")
101 | response = lrs.save_statements([statement, ret_statement])
102 |
103 | if not response:
104 | raise ValueError("statements failed to save")
105 | print("...done")
106 |
107 | # we can query our statements using an object
108 | # constructing the query object with common fields
109 | # note: more information about queries can be found in the API documentation:
110 | # docs/build/html/tincan.html#module-tincan.remote_lrs
111 | query = {
112 | "agent": actor,
113 | "verb": verb,
114 | "activity": object,
115 | "related_activities": True,
116 | "related_agents": True,
117 | "limit": 2,
118 | }
119 |
120 | print("querying statements...")
121 | response = lrs.query_statements(query)
122 |
123 | if not response:
124 | raise ValueError("statements could not be queried")
125 | print("...done")
126 |
127 | # now we will explore saving a document, e.g. a state document
128 | print("constructing a state document...")
129 | state_document = StateDocument(
130 | activity=object,
131 | agent=actor,
132 | id='stateDoc',
133 | content=bytearray('stateDocValue', encoding='utf-8'),
134 | )
135 | print("...done")
136 |
137 | print("saving state document...")
138 | response = lrs.save_state(state_document)
139 |
140 | if not response.success:
141 | raise ValueError("could not save state document")
142 | print("...done")
143 |
--------------------------------------------------------------------------------
/examples/resources/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RusticiSoftware/TinCanPython/bbc3f9dd5d8385e7b66c693e7f8262561392be74/examples/resources/__init__.py
--------------------------------------------------------------------------------
/examples/resources/lrs_properties.py.template:
--------------------------------------------------------------------------------
1 | """
2 | Contains user-specific information for testing.
3 | """
4 |
5 |
6 | endpoint=""
7 | version ="" # 1.0.1 | 1.0.0 | 0.95 | 0.9
8 | username=""
9 | password=""
10 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.md
3 |
4 | [pep8]
5 | max-line-length = 120
6 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup(
4 | name='tincan',
5 | packages=[
6 | 'tincan',
7 | 'tincan/conversions',
8 | 'tincan/documents',
9 | ],
10 | version='1.0.0',
11 | description='A Python library for implementing Tin Can API.',
12 | author='Rustici Software',
13 | author_email='mailto:support+tincanpython@tincanapi.com',
14 | maintainer='Brian J. Miller',
15 | maintainer_email='mailto:brian.miller@tincanapi.com',
16 | url='http://rusticisoftware.github.io/TinCanPython/',
17 | license='Apache License 2.0',
18 | keywords=[
19 | 'Tin Can',
20 | 'TinCan',
21 | 'Tin Can API',
22 | 'TinCanAPI',
23 | 'Experience API',
24 | 'xAPI',
25 | 'SCORM',
26 | 'AICC',
27 | ],
28 | install_requires=[
29 | 'aniso8601',
30 | 'pytz',
31 | ],
32 | )
33 |
--------------------------------------------------------------------------------
/test/__init__.py:
--------------------------------------------------------------------------------
1 | # empty for now
2 |
--------------------------------------------------------------------------------
/test/about_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import unittest
16 |
17 | if __name__ == '__main__':
18 | from test.main import setup_tincan_path
19 |
20 | setup_tincan_path()
21 | from test.test_utils import TinCanBaseTestCase
22 | from tincan import Version, About
23 |
24 |
25 | class AboutTest(TinCanBaseTestCase):
26 | def test_defaults(self):
27 | a = About()
28 | self.assertEqual(a.version, [Version.latest])
29 |
30 | def test_serialize_deserialize(self):
31 | a = About(version=['1.0.1', '1.0.0'], extensions={
32 | 'extension-a': 'http://www.example.com/ext/a',
33 | 'extension-b': 'http://www.example.com/ext/b',
34 | })
35 |
36 | self.assertEqual(a.version, ['1.0.1', '1.0.0'])
37 | self.assertIn('extension-a', a.extensions)
38 | self.assertIn('extension-b', a.extensions)
39 |
40 | self.assertSerializeDeserialize(a)
41 |
42 | def test_serialize_deserialize_init(self):
43 | data = {
44 | 'version': ['1.0.0'],
45 | 'extensions': {
46 | 'extension-a': 'http://www.example.com/ext/a',
47 | 'extension-b': 'http://www.example.com/ext/b',
48 | },
49 | }
50 |
51 | a = About(data)
52 |
53 | self.assertEqual(a.version, ['1.0.0'])
54 | self.assertIn('extension-a', a.extensions)
55 | self.assertIn('extension-b', a.extensions)
56 |
57 | self.assertSerializeDeserialize(a)
58 |
59 | def test_bad_property_init(self):
60 | with self.assertRaises(AttributeError):
61 | About(bad_name=2)
62 |
63 | with self.assertRaises(AttributeError):
64 | About({'bad_name': 2})
65 |
66 | def test_bad_version_init(self):
67 | About(version='1.0.1')
68 | About(version=['1.0.1'])
69 | About(version=['1.0.1', '1.0.0'])
70 |
71 | with self.assertRaises(ValueError):
72 | About(version='bad version')
73 |
74 | with self.assertRaises(ValueError):
75 | About(version=['bad version'])
76 |
77 | with self.assertRaises(ValueError):
78 | About(version=['1.0.1', 'bad version'])
79 |
80 | with self.assertRaises(ValueError):
81 | About(version=['1.0.1', 'bad version'])
82 |
83 |
84 | if __name__ == '__main__':
85 | suite = unittest.TestLoader().loadTestsFromTestCase(AboutTest)
86 | unittest.TextTestRunner(verbosity=2).run(suite)
87 |
--------------------------------------------------------------------------------
/test/activity_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | import json
15 | import unittest
16 |
17 | if __name__ == '__main__':
18 | from test.main import setup_tincan_path
19 |
20 | setup_tincan_path()
21 | from tincan import (
22 | Activity,
23 | ActivityDefinition,
24 | InteractionComponentList,
25 | LanguageMap,
26 | )
27 |
28 |
29 | class ActivityTest(unittest.TestCase):
30 | def test_InitEmpty(self):
31 | activity = Activity()
32 | self.assertIsNone(activity.id)
33 |
34 | def test_InitExceptionEmptyId(self):
35 | with self.assertRaises(ValueError):
36 | activity = Activity(id='')
37 |
38 | def test_Init(self):
39 | activity = Activity(id='test', definition=ActivityDefinition(), object_type='Activity')
40 | self.activityVerificationHelper(activity)
41 |
42 | def test_InitAnonDefinition(self):
43 | # these are arbitrary parameters - the ActivityDefinition is tested in ActivityDefinition_test
44 | activity = Activity(definition={'name': {'en-US': 'test'}, 'scale': []})
45 | self.assertIsInstance(activity.definition, ActivityDefinition)
46 | self.assertEqual(activity.definition.name, {'en-US': 'test'})
47 | self.assertIsInstance(activity.definition.name, LanguageMap)
48 | self.assertEqual(activity.definition.scale, [])
49 | self.assertIsInstance(activity.definition.scale, InteractionComponentList)
50 |
51 | def test_InitUnpack(self):
52 | obj = {'id': 'test', 'definition': ActivityDefinition(), 'object_type': 'Activity'}
53 | activity = Activity(**obj)
54 | self.activityVerificationHelper(activity)
55 |
56 | def test_InitUnpackExceptionEmptyId(self):
57 | obj = {'id': ''}
58 | with self.assertRaises(ValueError):
59 | activity = Activity(**obj)
60 |
61 | def test_FromJSON(self):
62 | activity = Activity.from_json('{"id": "test", "definition": {}, "object_type": "Activity"}')
63 | self.activityVerificationHelper(activity)
64 |
65 | def test_FromJSONExcpetionEmptyId(self):
66 | with self.assertRaises(ValueError):
67 | activity = Activity.from_json('{"id": ""}')
68 |
69 | def test_FromJSONExceptionEmptyObject(self):
70 | activity = Activity.from_json('{}')
71 | self.assertIsInstance(activity, Activity)
72 | self.assertIsNone(activity.id, None)
73 |
74 | def test_AsVersionEmpty(self):
75 | activity = Activity()
76 | activity2 = activity.as_version()
77 | self.assertEqual(activity2, {"objectType": "Activity"})
78 |
79 | def test_AsVersionNotEmpty(self):
80 | activity = Activity(id='test')
81 | activity2 = activity.as_version()
82 | self.assertEqual(activity2, {'id': 'test', "objectType": "Activity"})
83 |
84 | def test_AsVersion(self):
85 | activity = Activity(id='test', definition=ActivityDefinition(), object_type='Activity')
86 | activity2 = activity.as_version()
87 | self.assertEqual(activity2, {'id': 'test', 'definition': {}, 'objectType': 'Activity'})
88 |
89 | def test_ToJSONFromJSON(self):
90 | json_str = '{"id": "test", "definition": {}, "object_type": "Activity"}'
91 | check_str = '{"definition": {}, "id": "test", "objectType": "Activity"}'
92 | activity = Activity.from_json(json_str)
93 | self.activityVerificationHelper(activity)
94 | self.assertEqual(json.loads(activity.to_json()), json.loads(check_str))
95 |
96 | def test_ToJSON(self):
97 | check_str = '{"definition": {}, "id": "test", "objectType": "Activity"}'
98 | activity = Activity(**{'id': 'test', 'definition': {}, 'object_type': 'Activity'})
99 | self.assertEqual(json.loads(activity.to_json()), json.loads(check_str))
100 |
101 | def test_setDefinitionException(self):
102 | activity = Activity()
103 | with self.assertRaises(AttributeError):
104 | activity.definition = {"invalid": "definition"}
105 |
106 | def test_setDefinition(self):
107 | activity = Activity()
108 | activity.definition = ActivityDefinition()
109 | self.assertIsInstance(activity.definition, ActivityDefinition)
110 |
111 | def test_setObjectType(self):
112 | activity = Activity()
113 | activity.object_type = 'Activity'
114 | self.assertEqual(activity.object_type, 'Activity')
115 |
116 | def test_setIdException(self):
117 | activity = Activity()
118 | with self.assertRaises(ValueError):
119 | activity.id = ''
120 |
121 | def test_setId(self):
122 | activity = Activity()
123 | activity.id = 'test'
124 | self.assertEqual(activity.id, 'test')
125 |
126 | def activityVerificationHelper(self, activity):
127 | self.assertEqual(activity.id, 'test')
128 | self.assertIsInstance(activity.definition, ActivityDefinition)
129 | self.assertEqual(activity.object_type, 'Activity')
130 |
131 |
132 | if __name__ == '__main__':
133 | suite = unittest.TestLoader().loadTestsFromTestCase(ActivityTest)
134 | unittest.TextTestRunner(verbosity=2).run(suite)
135 |
--------------------------------------------------------------------------------
/test/context_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import unittest
16 |
17 | if __name__ == '__main__':
18 | from test.main import setup_tincan_path
19 |
20 | setup_tincan_path()
21 | from tincan import (
22 | Context,
23 | ContextActivities,
24 | Activity,
25 | Agent,
26 | StatementRef,
27 | Extensions,
28 | Group,
29 | )
30 | import uuid
31 |
32 |
33 | class ContextTest(unittest.TestCase):
34 | def test_InitEmpty(self):
35 | ctx = Context()
36 | self.assertIsInstance(ctx, Context)
37 | self.assertEqual(vars(ctx), {'_context_activities': None,
38 | '_extensions': None,
39 | '_instructor': None,
40 | '_language': None,
41 | '_platform': None,
42 | '_registration': None,
43 | '_revision': None,
44 | '_statement': None,
45 | '_team': None})
46 |
47 | def test_InitAll(self):
48 | ctx = Context(
49 | registration=uuid.uuid4(),
50 | instructor=Group(member=[Agent(name='instructorGroupMember')]),
51 | team=Group(member=[Agent(name='teamGroupMember')]),
52 | context_activities=ContextActivities(category=Activity(id='contextActivityCategory')),
53 | revision='revision',
54 | platform='platform',
55 | language='en-US',
56 | statement=StatementRef(id='016699c6-d600-48a7-96ab-86187498f16f'),
57 | extensions=Extensions({'extensions': 'extend!'})
58 | )
59 | self.ctxVerificationHelper(ctx)
60 |
61 | def test_InitUUIDFromString(self):
62 | reg = uuid.uuid4()
63 | """ Uses same regex as PHP """
64 | ctx = Context(registration=str(reg))
65 | self.assertEqual(ctx.registration, reg)
66 |
67 | def test_InitExceptionInvalidUUID(self):
68 | reg = 'not a valid uuid'
69 | with self.assertRaises(ValueError):
70 | Context(registration=reg)
71 |
72 | """ Try to break instructor, team, context_activities. See: test_InitException... in other test classes """
73 |
74 | def test_InitLanguages(self):
75 | language_ids = ['en', 'ast', 'zh-yue', 'ar-afb', 'zh-Hans', 'az-Latn', 'en-GB', 'es-005', 'zh-Hant-HK',
76 | 'sl-nedis', 'sl-IT-nedis', 'de-CH-1901', 'de-DE-u-co-phonebk', 'en-US-x-twain']
77 | for tag in language_ids:
78 | ctx = Context(language=tag)
79 | self.assertEqual(ctx.language, tag)
80 | self.assertIsInstance(ctx, Context)
81 |
82 | def test_InitExceptionInvalidLanguage(self):
83 | regional_id = 'In-valiD-Code'
84 | with self.assertRaises(ValueError):
85 | Context(language=regional_id)
86 |
87 | """ Statement Ref tests - will be trival """
88 |
89 | def test_FromJSONExceptionBadJSON(self):
90 | with self.assertRaises(ValueError):
91 | Context.from_json('{"bad JSON"}')
92 |
93 | def test_FromJSONExceptionMalformedJSON(self):
94 | with self.assertRaises(AttributeError):
95 | Context.from_json('{"test": "invalid property"}')
96 |
97 | def test_FromJSONExceptionPartiallyMalformedJSON(self):
98 | with self.assertRaises(AttributeError):
99 | Context.from_json('{"test": "invalid property", "id": \
100 | "valid property"}')
101 |
102 | def test_FromJSON(self):
103 | json_str = '{\
104 | "registration": "016699c6-d600-48a7-96ab-86187498f16f",\
105 | "instructor": {"member": [{"name": "instructorGroupMember"}]},\
106 | "team": {"member": [{"name": "teamGroupMember"}]},\
107 | "context_activities": {"category": {"id": "contextActivityCategory"}},\
108 | "revision": "revision",\
109 | "platform": "platform",\
110 | "language": "en-US",\
111 | "extensions": {"extensions": "extend!"}}'
112 | ctx = Context.from_json(json_str)
113 | self.ctxVerificationHelper(ctx)
114 |
115 | def test_AsVersion(self):
116 | obj = {
117 | "registration": "016699c6-d600-48a7-96ab-86187498f16f",
118 | "instructor": {"member": [{"name": "instructorGroupMember"}]},
119 | "team": {"member": [{"name": "teamGroupMember"}]},
120 | "context_activities": {"category": {"id": "contextActivityCategory"}},
121 | "revision": "revision",
122 | "platform": "platform",
123 | "language": "en-US",
124 | "extensions": {"extensions": "extend!"}
125 | }
126 | """ Keys are corrected, and ContextActivities is properly listified """
127 | check_obj = {
128 | "registration": "016699c6-d600-48a7-96ab-86187498f16f",
129 | "instructor": {"member": [{"name": "instructorGroupMember", "objectType": "Agent"}], "objectType": "Group"},
130 | "team": {"member": [{"name": "teamGroupMember", "objectType": "Agent"}], "objectType": "Group"},
131 | "contextActivities": {"category": [{"id": "contextActivityCategory", "objectType": "Activity"}]},
132 | "revision": "revision",
133 | "platform": "platform",
134 | "language": "en-US",
135 | "extensions": {"extensions": "extend!"}
136 | }
137 | ctx = Context(**obj)
138 | ctx2 = ctx.as_version()
139 | self.assertEqual(ctx2, check_obj)
140 |
141 | def ctxVerificationHelper(self, ctx):
142 | self.assertIsInstance(ctx, Context)
143 | self.assertIsInstance(ctx.registration, uuid.UUID)
144 | self.assertIsInstance(ctx.instructor, Group)
145 | self.assertEqual(ctx.instructor.member[0].name, 'instructorGroupMember')
146 | self.assertIsInstance(ctx.team, Group)
147 | self.assertEqual(ctx.team.member[0].name, 'teamGroupMember')
148 | self.assertIsInstance(ctx.context_activities, ContextActivities)
149 | self.assertEqual(ctx.context_activities.category[0].id, 'contextActivityCategory')
150 | self.assertEqual(ctx.revision, 'revision')
151 | self.assertEqual(ctx.platform, 'platform')
152 | self.assertEqual(ctx.language, 'en-US')
153 | self.assertIsInstance(ctx.extensions, Extensions)
154 | self.assertEqual(ctx.extensions['extensions'], 'extend!')
155 |
156 |
157 | if __name__ == '__main__':
158 | suite = unittest.TestLoader().loadTestsFromTestCase(ContextTest)
159 | unittest.TextTestRunner(verbosity=2).run(suite)
160 |
--------------------------------------------------------------------------------
/test/conversions/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RusticiSoftware/TinCanPython/bbc3f9dd5d8385e7b66c693e7f8262561392be74/test/conversions/__init__.py
--------------------------------------------------------------------------------
/test/documents/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RusticiSoftware/TinCanPython/bbc3f9dd5d8385e7b66c693e7f8262561392be74/test/documents/__init__.py
--------------------------------------------------------------------------------
/test/documents/activity_profile_document_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import unittest
16 | from datetime import datetime
17 |
18 | import pytz
19 |
20 |
21 | if __name__ == '__main__':
22 | import sys
23 | from os.path import dirname, abspath
24 |
25 | sys.path.insert(0, dirname(dirname(dirname(abspath(__file__)))))
26 | from test.main import setup_tincan_path
27 |
28 | setup_tincan_path()
29 | from tincan import (
30 | ActivityProfileDocument,
31 | Activity,
32 | ActivityDefinition,
33 | LanguageMap,
34 | )
35 |
36 |
37 | class ActivityProfileDocumentTest(unittest.TestCase):
38 | def setUp(self):
39 | self.activity = Activity(
40 | id="http://tincanapi.com/TinCanPython/Test/Unit/0",
41 | definition=ActivityDefinition()
42 | )
43 | self.activity.definition.type = "http://id.tincanapi.com/activitytype/unit-test"
44 | self.activity.definition.name = LanguageMap({"en-US": "Python Tests"})
45 | self.activity.definition.description = LanguageMap(
46 | {"en-US": "Unit test in the test suite for the Python library"}
47 | )
48 |
49 | def tearDown(self):
50 | pass
51 |
52 | def test_init_empty(self):
53 | doc = ActivityProfileDocument()
54 | self.assertIsInstance(doc, ActivityProfileDocument)
55 | self.assertTrue(hasattr(doc, "id"))
56 | self.assertIsNone(doc.id)
57 | self.assertTrue(hasattr(doc, "content_type"))
58 | self.assertIsNone(doc.content_type)
59 | self.assertTrue(hasattr(doc, "content"))
60 | self.assertIsNone(doc.content)
61 | self.assertTrue(hasattr(doc, "etag"))
62 | self.assertIsNone(doc.etag)
63 | self.assertTrue(hasattr(doc, "timestamp"))
64 | self.assertIsNone(doc.timestamp)
65 | self.assertTrue(hasattr(doc, "activity"))
66 | self.assertIsNone(doc.activity)
67 |
68 | def test_init_kwarg_exception(self):
69 | with self.assertRaises(AttributeError):
70 | ActivityProfileDocument(bad_test="test")
71 |
72 | def test_init_arg_exception_dict(self):
73 | d = {"bad_test": "test", "id": "ok"}
74 | with self.assertRaises(AttributeError):
75 | ActivityProfileDocument(d)
76 |
77 | def test_init_arg_exception_obj(self):
78 | class Tester(object):
79 | def __init__(self, id=None, bad_test="test"):
80 | self.id = id
81 | self.bad_test = bad_test
82 |
83 | obj = Tester()
84 |
85 | with self.assertRaises(AttributeError):
86 | ActivityProfileDocument(obj)
87 |
88 | def test_init_partial(self):
89 | doc = ActivityProfileDocument(id="test", content_type="test type")
90 | self.assertEqual(doc.id, "test")
91 | self.assertEqual(doc.content_type, "test type")
92 | self.assertTrue(hasattr(doc, "content"))
93 | self.assertTrue(hasattr(doc, "etag"))
94 | self.assertTrue(hasattr(doc, "timestamp"))
95 | self.assertTrue(hasattr(doc, "activity"))
96 |
97 | def test_init_all(self):
98 | doc = ActivityProfileDocument(
99 | id="test",
100 | content_type="test type",
101 | content=bytearray("test bytearray", "utf-8"),
102 | etag="test etag",
103 | timestamp="2014-06-23T15:25:00-05:00",
104 | activity=self.activity,
105 | )
106 | self.assertEqual(doc.id, "test")
107 | self.assertEqual(doc.content_type, "test type")
108 | self.assertEqual(doc.content, bytearray("test bytearray", "utf-8"))
109 | self.assertEqual(doc.etag, "test etag")
110 |
111 | central = pytz.timezone("US/Central") # UTC -0500
112 | dt = central.localize(datetime(2014, 6, 23, 15, 25))
113 | self.assertEqual(doc.timestamp, dt)
114 | self.assertEqual(doc.activity, self.activity)
115 |
116 | def test_setters(self):
117 | doc = ActivityProfileDocument()
118 | doc.id = "test"
119 | doc.content_type = "test type"
120 | doc.content = bytearray("test bytearray", "utf-8")
121 | doc.etag = "test etag"
122 | doc.timestamp = "2014-06-23T15:25:00-05:00"
123 | doc.activity = self.activity
124 |
125 | self.assertEqual(doc.id, "test")
126 | self.assertEqual(doc.content_type, "test type")
127 | self.assertEqual(doc.content, bytearray("test bytearray", "utf-8"))
128 | self.assertEqual(doc.etag, "test etag")
129 |
130 | central = pytz.timezone("US/Central") # UTC -0500
131 | dt = central.localize(datetime(2014, 6, 23, 15, 25))
132 | self.assertEqual(doc.timestamp, dt)
133 | self.assertEqual(doc.activity, self.activity)
134 |
135 | def test_setters_none(self):
136 | doc = ActivityProfileDocument()
137 | doc.id = None
138 | doc.content_type = None
139 | doc.content = None
140 | doc.etag = None
141 | doc.timestamp = None
142 | doc.activity = None
143 |
144 | self.assertIsNone(doc.id)
145 | self.assertIsNone(doc.content_type)
146 | self.assertIsNone(doc.content)
147 | self.assertIsNone(doc.etag)
148 | self.assertIsNone(doc.timestamp)
149 | self.assertIsNone(doc.activity)
150 |
151 | def test_activity_setter(self):
152 | doc = ActivityProfileDocument()
153 | doc.activity = {"id": "http://tincanapi.com/TinCanPython/Test/Unit/0"}
154 |
155 | self.assertEqual(doc.activity.id, "http://tincanapi.com/TinCanPython/Test/Unit/0")
156 |
157 |
158 | if __name__ == "__main__":
159 | suite = unittest.TestLoader().loadTestsFromTestCase(ActivityProfileDocumentTest)
160 | unittest.TextTestRunner(verbosity=2).run(suite)
161 |
--------------------------------------------------------------------------------
/test/documents/agent_profile_document_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import unittest
16 | from datetime import datetime
17 |
18 | import pytz
19 |
20 |
21 | if __name__ == '__main__':
22 | import sys
23 | from os.path import dirname, abspath
24 |
25 | sys.path.insert(0, dirname(dirname(dirname(abspath(__file__)))))
26 | from test.main import setup_tincan_path
27 |
28 | setup_tincan_path()
29 | from tincan import AgentProfileDocument, Agent
30 |
31 |
32 | class AgentProfileDocumentTest(unittest.TestCase):
33 | def setUp(self):
34 | self.agent = Agent(mbox="mailto:tincanpython@tincanapi.com")
35 |
36 | def tearDown(self):
37 | pass
38 |
39 | def test_init_empty(self):
40 | doc = AgentProfileDocument()
41 | self.assertIsInstance(doc, AgentProfileDocument)
42 | self.assertTrue(hasattr(doc, "id"))
43 | self.assertIsNone(doc.id)
44 | self.assertTrue(hasattr(doc, "content_type"))
45 | self.assertIsNone(doc.content_type)
46 | self.assertTrue(hasattr(doc, "content"))
47 | self.assertIsNone(doc.content)
48 | self.assertTrue(hasattr(doc, "etag"))
49 | self.assertIsNone(doc.etag)
50 | self.assertTrue(hasattr(doc, "timestamp"))
51 | self.assertIsNone(doc.timestamp)
52 | self.assertTrue(hasattr(doc, "agent"))
53 | self.assertIsNone(doc.agent)
54 |
55 | def test_init_kwarg_exception(self):
56 | with self.assertRaises(AttributeError):
57 | AgentProfileDocument(bad_test="test")
58 |
59 | def test_init_arg_exception_dict(self):
60 | d = {"bad_test": "test", "id": "ok"}
61 | with self.assertRaises(AttributeError):
62 | AgentProfileDocument(d)
63 |
64 | def test_init_arg_exception_obj(self):
65 | class Tester(object):
66 | def __init__(self, id=None, bad_test="test"):
67 | self.id = id
68 | self.bad_test = bad_test
69 |
70 | obj = Tester()
71 |
72 | with self.assertRaises(AttributeError):
73 | AgentProfileDocument(obj)
74 |
75 | def test_init_partial(self):
76 | doc = AgentProfileDocument(id="test", content_type="test type")
77 | self.assertEqual(doc.id, "test")
78 | self.assertEqual(doc.content_type, "test type")
79 | self.assertTrue(hasattr(doc, "content"))
80 | self.assertTrue(hasattr(doc, "etag"))
81 | self.assertTrue(hasattr(doc, "timestamp"))
82 | self.assertTrue(hasattr(doc, "agent"))
83 |
84 | def test_init_all(self):
85 | doc = AgentProfileDocument(
86 | id="test",
87 | content_type="test type",
88 | content=bytearray("test bytearray", "utf-8"),
89 | etag="test etag",
90 | timestamp="2014-06-23T15:25:00-05:00",
91 | agent=self.agent,
92 | )
93 | self.assertEqual(doc.id, "test")
94 | self.assertEqual(doc.content_type, "test type")
95 | self.assertEqual(doc.content, bytearray("test bytearray", "utf-8"))
96 | self.assertEqual(doc.etag, "test etag")
97 |
98 | central = pytz.timezone("US/Central") # UTC -0500
99 | dt = central.localize(datetime(2014, 6, 23, 15, 25))
100 | self.assertEqual(doc.timestamp, dt)
101 | self.assertEqual(doc.agent, self.agent)
102 |
103 | def test_setters(self):
104 | doc = AgentProfileDocument()
105 | doc.id = "test"
106 | doc.content_type = "test type"
107 | doc.content = bytearray("test bytearray", "utf-8")
108 | doc.etag = "test etag"
109 | doc.timestamp = "2014-06-23T15:25:00-05:00"
110 | doc.agent = self.agent
111 |
112 | self.assertEqual(doc.id, "test")
113 | self.assertEqual(doc.content_type, "test type")
114 | self.assertEqual(doc.content, bytearray("test bytearray", "utf-8"))
115 | self.assertEqual(doc.etag, "test etag")
116 |
117 | central = pytz.timezone("US/Central") # UTC -0500
118 | dt = central.localize(datetime(2014, 6, 23, 15, 25))
119 | self.assertEqual(doc.timestamp, dt)
120 | self.assertEqual(doc.agent, self.agent)
121 |
122 | def test_setters_none(self):
123 | doc = AgentProfileDocument()
124 | doc.id = None
125 | doc.content_type = None
126 | doc.content = None
127 | doc.etag = None
128 | doc.timestamp = None
129 | doc.agent = None
130 |
131 | self.assertIsNone(doc.id)
132 | self.assertIsNone(doc.content_type)
133 | self.assertIsNone(doc.content)
134 | self.assertIsNone(doc.etag)
135 | self.assertIsNone(doc.timestamp)
136 | self.assertIsNone(doc.agent)
137 |
138 | def test_agent_setter(self):
139 | doc = AgentProfileDocument()
140 | doc.agent = {"mbox": "mailto:tincanpython@tincanapi.com"}
141 | self.assertIsInstance(doc.agent, Agent)
142 | self.assertEqual(doc.agent.mbox, self.agent.mbox)
143 |
144 |
145 | if __name__ == "__main__":
146 | suite = unittest.TestLoader().loadTestsFromTestCase(AgentProfileDocumentTest)
147 | unittest.TextTestRunner(verbosity=2).run(suite)
148 |
--------------------------------------------------------------------------------
/test/documents/document_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import unittest
16 | import datetime
17 |
18 | import pytz
19 |
20 |
21 | if __name__ == '__main__':
22 | import sys
23 | from os.path import dirname, abspath
24 |
25 | sys.path.insert(0, dirname(dirname(dirname(abspath(__file__)))))
26 | from test.main import setup_tincan_path
27 |
28 | setup_tincan_path()
29 | from tincan import Document
30 |
31 |
32 | class DocumentTest(unittest.TestCase):
33 | def setUp(self):
34 | pass
35 |
36 | def tearDown(self):
37 | pass
38 |
39 | def test_init_empty(self):
40 | doc = Document()
41 | self.assertIsInstance(doc, Document)
42 | self.assertTrue(hasattr(doc, "id"))
43 | self.assertIsNone(doc.id)
44 | self.assertTrue(hasattr(doc, "content_type"))
45 | self.assertIsNone(doc.content_type)
46 | self.assertTrue(hasattr(doc, "content"))
47 | self.assertIsNone(doc.content)
48 | self.assertTrue(hasattr(doc, "etag"))
49 | self.assertIsNone(doc.etag)
50 | self.assertTrue(hasattr(doc, "timestamp"))
51 | self.assertIsNone(doc.timestamp)
52 |
53 | def test_init_kwarg_exception(self):
54 | with self.assertRaises(AttributeError):
55 | Document(bad_test="test")
56 |
57 | def test_init_arg_exception_dict(self):
58 | d = {"bad_test": "test", "id": "ok"}
59 | with self.assertRaises(AttributeError):
60 | Document(d)
61 |
62 | def test_init_arg_exception_obj(self):
63 | class Tester(object):
64 | def __init__(self, id=None, bad_test="test"):
65 | self.id = id
66 | self.bad_test = bad_test
67 |
68 | obj = Tester()
69 |
70 | with self.assertRaises(AttributeError):
71 | Document(obj)
72 |
73 | def test_init_partial(self):
74 | doc = Document(id="test", content_type="test type")
75 | self.assertEqual(doc.id, "test")
76 | self.assertEqual(doc.content_type, "test type")
77 | self.assertTrue(hasattr(doc, "content"))
78 | self.assertTrue(hasattr(doc, "etag"))
79 | self.assertTrue(hasattr(doc, "timestamp"))
80 |
81 | def test_init_all(self):
82 | doc = Document(
83 | id="test",
84 | content_type="test type",
85 | content=bytearray("test bytearray", "utf-8"),
86 | etag="test etag",
87 | timestamp="2014-06-23T15:25:00-05:00"
88 | )
89 |
90 | self.assertEqual(doc.id, "test")
91 | self.assertEqual(doc.content_type, "test type")
92 | self.assertEqual(doc.content, bytearray("test bytearray", "utf-8"))
93 | self.assertEqual(doc.etag, "test etag")
94 |
95 | central = pytz.timezone("US/Central") # UTC -0500
96 | dt = central.localize(datetime.datetime(2014, 6, 23, 15, 25))
97 | self.assertEqual(doc.timestamp, dt)
98 |
99 | def test_setters(self):
100 | doc = Document()
101 | doc.id = "test"
102 | doc.content_type = "test type"
103 | doc.content = bytearray("test bytearray", "utf-8")
104 | doc.etag = "test etag"
105 | doc.timestamp = "2014-06-23T15:25:00-05:00"
106 |
107 | self.assertEqual(doc.id, "test")
108 | self.assertEqual(doc.content_type, "test type")
109 | self.assertEqual(doc.content, bytearray("test bytearray", "utf-8"))
110 | self.assertEqual(doc.etag, "test etag")
111 |
112 | central = pytz.timezone("US/Central") # UTC -0500
113 | dt = central.localize(datetime.datetime(2014, 6, 23, 15, 25))
114 | self.assertEqual(doc.timestamp, dt)
115 |
116 | def test_setters_none(self):
117 | doc = Document()
118 | doc.id = None
119 | doc.content_type = None
120 | doc.content = None
121 | doc.etag = None
122 | doc.timestamp = None
123 |
124 | self.assertIsNone(doc.id)
125 | self.assertIsNone(doc.content_type)
126 | self.assertIsNone(doc.content)
127 | self.assertIsNone(doc.etag)
128 | self.assertIsNone(doc.timestamp)
129 |
130 | def test_content_setter(self):
131 | doc = Document()
132 | doc.content = "test bytearray"
133 | self.assertEqual(doc.content, bytearray("test bytearray", "utf-8"))
134 |
135 | def test_content_timestamp(self):
136 | doc = Document()
137 | dt = pytz.utc.localize(datetime.datetime.utcnow())
138 | doc.timestamp = dt
139 | self.assertEqual(doc.timestamp, dt)
140 |
141 |
142 | if __name__ == "__main__":
143 | suite = unittest.TestLoader().loadTestsFromTestCase(DocumentTest)
144 | unittest.TextTestRunner(verbosity=2).run(suite)
145 |
--------------------------------------------------------------------------------
/test/documents/state_document_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import unittest
16 | from datetime import datetime
17 |
18 | import pytz
19 |
20 |
21 | if __name__ == '__main__':
22 | import sys
23 | from os.path import dirname, abspath
24 |
25 | sys.path.insert(0, dirname(dirname(dirname(abspath(__file__)))))
26 | from test.main import setup_tincan_path
27 |
28 | setup_tincan_path()
29 | from tincan import (
30 | StateDocument,
31 | Agent,
32 | Activity,
33 | ActivityDefinition,
34 | LanguageMap
35 | )
36 |
37 |
38 | class StateDocumentTest(unittest.TestCase):
39 | def setUp(self):
40 | self.agent = Agent(mbox="mailto:tincanpython@tincanapi.com")
41 |
42 | self.activity = Activity(
43 | id="http://tincanapi.com/TinCanPython/Test/Unit/0",
44 | definition=ActivityDefinition()
45 | )
46 | self.activity.definition.type = "http://id.tincanapi.com/activitytype/unit-test"
47 | self.activity.definition.name = LanguageMap({"en-US": "Python Tests"})
48 | self.activity.definition.description = LanguageMap(
49 | {"en-US": "Unit test in the test suite for the Python library"}
50 | )
51 |
52 | def tearDown(self):
53 | pass
54 |
55 | def test_init_empty(self):
56 | doc = StateDocument()
57 | self.assertIsInstance(doc, StateDocument)
58 | self.assertTrue(hasattr(doc, "id"))
59 | self.assertIsNone(doc.id)
60 | self.assertTrue(hasattr(doc, "content_type"))
61 | self.assertIsNone(doc.content_type)
62 | self.assertTrue(hasattr(doc, "content"))
63 | self.assertIsNone(doc.content)
64 | self.assertTrue(hasattr(doc, "etag"))
65 | self.assertIsNone(doc.etag)
66 | self.assertTrue(hasattr(doc, "timestamp"))
67 | self.assertIsNone(doc.timestamp)
68 | self.assertTrue(hasattr(doc, "activity"))
69 | self.assertIsNone(doc.activity)
70 | self.assertTrue(hasattr(doc, "agent"))
71 | self.assertIsNone(doc.agent)
72 | self.assertTrue(hasattr(doc, "registration"))
73 | self.assertIsNone(doc.registration)
74 |
75 | def test_init_kwarg_exception(self):
76 | with self.assertRaises(AttributeError):
77 | StateDocument(bad_test="test")
78 |
79 | def test_init_arg_exception_dict(self):
80 | d = {"bad_test": "test", "id": "ok"}
81 | with self.assertRaises(AttributeError):
82 | StateDocument(d)
83 |
84 | def test_init_arg_exception_obj(self):
85 | class Tester(object):
86 | def __init__(self, id=None, bad_test="test"):
87 | self.id = id
88 | self.bad_test = bad_test
89 |
90 | obj = Tester()
91 |
92 | with self.assertRaises(AttributeError):
93 | StateDocument(obj)
94 |
95 | def test_init_partial(self):
96 | doc = StateDocument(id="test", content_type="test type")
97 | self.assertEqual(doc.id, "test")
98 | self.assertEqual(doc.content_type, "test type")
99 | self.assertTrue(hasattr(doc, "content"))
100 | self.assertTrue(hasattr(doc, "etag"))
101 | self.assertTrue(hasattr(doc, "timestamp"))
102 | self.assertTrue(hasattr(doc, "agent"))
103 | self.assertTrue(hasattr(doc, "activity"))
104 | self.assertTrue(hasattr(doc, "registration"))
105 |
106 | def test_init_all(self):
107 | doc = StateDocument(
108 | id="test",
109 | content_type="test type",
110 | content=bytearray("test bytearray", "utf-8"),
111 | etag="test etag",
112 | timestamp="2014-06-23T15:25:00-05:00",
113 | agent=self.agent,
114 | activity=self.activity,
115 | registration="test registration"
116 | )
117 | self.assertEqual(doc.id, "test")
118 | self.assertEqual(doc.content_type, "test type")
119 | self.assertEqual(doc.content, bytearray("test bytearray", "utf-8"))
120 | self.assertEqual(doc.etag, "test etag")
121 |
122 | central = pytz.timezone("US/Central") # UTC -0500
123 | dt = central.localize(datetime(2014, 6, 23, 15, 25))
124 | self.assertEqual(doc.timestamp, dt)
125 | self.assertEqual(doc.agent, self.agent)
126 | self.assertEqual(doc.activity, self.activity)
127 | self.assertEqual(doc.registration, "test registration")
128 |
129 | def test_setters(self):
130 | doc = StateDocument()
131 | doc.id = "test"
132 | doc.content_type = "test type"
133 | doc.content = bytearray("test bytearray", "utf-8")
134 | doc.etag = "test etag"
135 | doc.timestamp = "2014-06-23T15:25:00-05:00"
136 | doc.agent = self.agent
137 | doc.activity = self.activity
138 | doc.registration = "test registration"
139 |
140 | self.assertEqual(doc.id, "test")
141 | self.assertEqual(doc.content_type, "test type")
142 | self.assertEqual(doc.content, bytearray("test bytearray", "utf-8"))
143 | self.assertEqual(doc.etag, "test etag")
144 |
145 | central = pytz.timezone("US/Central") # UTC -0500
146 | dt = central.localize(datetime(2014, 6, 23, 15, 25))
147 | self.assertEqual(doc.timestamp, dt)
148 | self.assertEqual(doc.agent, self.agent)
149 | self.assertEqual(doc.activity, self.activity)
150 | self.assertEqual(doc.registration, "test registration")
151 |
152 | def test_setters_none(self):
153 | doc = StateDocument()
154 | doc.id = None
155 | doc.content_type = None
156 | doc.content = None
157 | doc.etag = None
158 | doc.timestamp = None
159 | doc.agent = None
160 | doc.activity = None
161 | doc.registration = None
162 |
163 | self.assertIsNone(doc.id)
164 | self.assertIsNone(doc.content_type)
165 | self.assertIsNone(doc.content)
166 | self.assertIsNone(doc.etag)
167 | self.assertIsNone(doc.timestamp)
168 | self.assertIsNone(doc.agent)
169 | self.assertIsNone(doc.activity)
170 | self.assertIsNone(doc.registration)
171 |
172 | def test_agent_setter(self):
173 | doc = StateDocument()
174 | doc.agent = {"mbox": "mailto:tincanpython@tincanapi.com"}
175 | self.assertIsInstance(doc.agent, Agent)
176 | self.assertEqual(doc.agent.mbox, self.agent.mbox)
177 |
178 | def test_activity_setter(self):
179 | doc = StateDocument()
180 | doc.activity = {"id": "http://tincanapi.com/TinCanPython/Test/Unit/0"}
181 |
182 | self.assertEqual(doc.activity.id, "http://tincanapi.com/TinCanPython/Test/Unit/0")
183 |
184 |
185 | if __name__ == "__main__":
186 | suite = unittest.TestLoader().loadTestsFromTestCase(StateDocumentTest)
187 | unittest.TextTestRunner(verbosity=2).run(suite)
188 |
--------------------------------------------------------------------------------
/test/extensions_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import unittest
16 |
17 | if __name__ == '__main__':
18 | from test.main import setup_tincan_path
19 |
20 | setup_tincan_path()
21 | from tincan import Extensions
22 | from test.test_utils import TinCanBaseTestCase
23 |
24 |
25 | class ExtensionsTest(TinCanBaseTestCase):
26 | def test_serialize_deserialize(self):
27 | ext = Extensions()
28 | ext['http://example.com/string'] = 'extensionValue'
29 | ext['http://example.com/int'] = 10
30 | ext['http://example.com/double'] = 1.897
31 |
32 | # ext['http://example.com/object'] = get_agent('Random', 'mbox', 'mailto:random@example.com')
33 |
34 | self.assertSerializeDeserialize(ext)
35 |
36 | def test_serialize_deserialize_init(self):
37 | data = {
38 | 'http://example.com/string': 'extensionValue',
39 | 'http://example.com/int': 10,
40 | 'http://example.com/double': 1.897,
41 | # 'http://example.com/object': get_agent('Random', 'mbox', 'mailto:random@example.com'),
42 | }
43 |
44 | ext = Extensions(data)
45 | self.assertSerializeDeserialize(ext)
46 |
47 | def test_read_write(self):
48 | ext = Extensions()
49 | self.assertEqual(len(ext), 0, 'Empty Extensions inited as non-empty!')
50 |
51 | ext['http://example.com/int'] = 10
52 | self.assertIn('http://example.com/int', ext, 'Could not add item to Extensions!')
53 | self.assertEqual(10, ext['http://example.com/int'])
54 | self.assertEqual(len(ext), 1, 'Extensions is the wrong size!')
55 |
56 | ext['http://example.com/int'] += 5
57 | self.assertEqual(15, ext['http://example.com/int'], 'Could not modify item in Extensions!')
58 |
59 | del ext['http://example.com/int']
60 | self.assertNotIn('http://example.com/int', ext, 'Could not delete item from Extensions!')
61 | self.assertEqual(len(ext), 0, 'Could not empty the Extensions object!')
62 |
63 |
64 | if __name__ == '__main__':
65 | suite = unittest.TestLoader().loadTestsFromTestCase(ExtensionsTest)
66 | unittest.TextTestRunner(verbosity=2).run(suite)
67 |
--------------------------------------------------------------------------------
/test/group_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | import json
15 | import unittest
16 |
17 | if __name__ == '__main__':
18 | from test.main import setup_tincan_path
19 |
20 | setup_tincan_path()
21 | from tincan import Group, Agent
22 |
23 |
24 | class GroupTest(unittest.TestCase):
25 | def test_InitEmpty(self):
26 | group = Group()
27 | self.assertEqual(group.member, [])
28 |
29 | def test_InitObjectType(self):
30 | group = Group(object_type='Group')
31 | self.assertEqual(group.object_type, 'Group')
32 | self.assertEqual(group.member, [])
33 |
34 | def test_InitMember(self):
35 | group = Group(member=[Agent(name='test')])
36 | self.assertIsInstance(group.member[0], Agent)
37 |
38 | def test_InitMemberAnon(self):
39 | group = Group(member=[{"name": "test"}])
40 | self.assertIsInstance(group.member[0], Agent)
41 |
42 | def test_FromJSONExceptionEmpty(self):
43 | with self.assertRaises(ValueError):
44 | Group.from_json('')
45 |
46 | def test_FromJSONEmptyObject(self):
47 | group = Group.from_json('{}')
48 | self.assertEqual(group.member, [])
49 |
50 | def test_FromJSONmember(self):
51 | group = Group.from_json('''{"member":[{"name":"test"}]}''')
52 | for k in group.member:
53 | self.assertIsInstance(k, Agent)
54 |
55 | def test_FromJSONExceptionBadJSON(self):
56 | with self.assertRaises(ValueError):
57 | Group.from_json('{"bad JSON"}')
58 |
59 | def test_AddMemberAnon(self):
60 | group = Group()
61 | group.addmember({"name": "test"})
62 | self.assertIsInstance(group.member[0], Agent)
63 |
64 | def test_AddMember(self):
65 | group = Group()
66 | group.addmember(Agent(name='test'))
67 | self.assertIsInstance(group.member[0], Agent)
68 |
69 | def test_InitUnpack(self):
70 | obj = {"member": [{"name": "test"}]}
71 | group = Group(**obj)
72 | self.assertIsInstance(group.member[0], Agent)
73 |
74 | def test_ToJSONFromJSON(self):
75 | group = Group.from_json('{"member":[{"name":"test"}, {"name":"test2"}]}')
76 | self.assertIsInstance(group.member[0], Agent)
77 | self.assertEqual(json.loads(group.to_json()),
78 | json.loads('{"member": [{"name": "test", "objectType": "Agent"}, '
79 | '{"name": "test2", "objectType": "Agent"}], "objectType": "Group"}'))
80 |
81 | def test_ToJSON(self):
82 | group = Group(**{'member': [{'name': 'test'}, {'name': 'test2'}]})
83 | self.assertEqual(json.loads(group.to_json()),
84 | json.loads('{"member": [{"name": "test", "objectType": "Agent"}, '
85 | '{"name": "test2", "objectType": "Agent"}], "objectType": "Group"}'))
86 |
87 |
88 | if __name__ == '__main__':
89 | suite = unittest.TestLoader().loadTestsFromTestCase(GroupTest)
90 | unittest.TextTestRunner(verbosity=2).run(suite)
91 |
--------------------------------------------------------------------------------
/test/http_request_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import unittest
16 |
17 | if __name__ == '__main__':
18 | from test.main import setup_tincan_path
19 |
20 | setup_tincan_path()
21 | from tincan import HTTPRequest
22 |
23 |
24 | class HTTPRequestTest(unittest.TestCase):
25 | def setUp(self):
26 | pass
27 |
28 | def tearDown(self):
29 | pass
30 |
31 | def test_init_empty(self):
32 | req = HTTPRequest()
33 | self.assertIsInstance(req, HTTPRequest)
34 | self.assertIsNone(req.content)
35 | self.assertIsNone(req.ignore404)
36 |
37 | self.assertTrue(hasattr(req, "method"))
38 | self.assertIsNone(req.method)
39 |
40 | self.assertTrue(hasattr(req, "resource"))
41 | self.assertIsNone(req.resource)
42 |
43 | self.assertTrue(hasattr(req, "headers"))
44 | self.assertEqual(req.headers, {})
45 |
46 | self.assertTrue(hasattr(req, "query_params"))
47 | self.assertEqual(req.query_params, {})
48 |
49 | def test_init_kwarg_exception(self):
50 | with self.assertRaises(AttributeError):
51 | HTTPRequest(bad_test="test")
52 |
53 | def test_init_arg_exception_dict(self):
54 | d = {"bad_test": "test", "resource": "ok"}
55 | with self.assertRaises(AttributeError):
56 | HTTPRequest(d)
57 |
58 | def test_init_arg_exception_obj(self):
59 | class Tester(object):
60 | def __init__(self, resource="ok", bad_test="test"):
61 | self.resource = resource
62 | self.bad_test = bad_test
63 |
64 | obj = Tester()
65 |
66 | with self.assertRaises(AttributeError):
67 | HTTPRequest(obj)
68 |
69 | def test_init_partial(self):
70 | req = HTTPRequest(
71 | method="method test",
72 | query_params={"test": "val"}
73 | )
74 | self.assertIsInstance(req, HTTPRequest)
75 |
76 | self.assertEqual(req.method, "method test")
77 | self.assertEqual(req.query_params, {"test": "val"})
78 |
79 | self.assertIsNone(req.content)
80 | self.assertIsNone(req.ignore404)
81 |
82 | self.assertTrue(hasattr(req, "resource"))
83 | self.assertIsNone(req.resource)
84 |
85 | self.assertTrue(hasattr(req, "headers"))
86 | self.assertEqual(req.headers, {})
87 |
88 | def test_init_all(self):
89 | req = HTTPRequest(
90 | method="method test",
91 | resource="resource test",
92 | headers={"test": "val"},
93 | query_params={"test": "val"},
94 | content="content test",
95 | ignore404=True,
96 | )
97 | self.assertIsInstance(req, HTTPRequest)
98 |
99 | self.assertEqual(req.method, "method test")
100 | self.assertEqual(req.resource, "resource test")
101 | self.assertEqual(req.headers, {"test": "val"})
102 | self.assertEqual(req.query_params, {"test": "val"})
103 | self.assertEqual(req.content, "content test")
104 | self.assertTrue(req.ignore404)
105 |
106 | def test_setters(self):
107 | req = HTTPRequest()
108 |
109 | req.method = "method test"
110 | req.resource = "resource test"
111 | req.headers = {"test": "val"}
112 | req.query_params = {"test": "val"}
113 | req.content = "content test"
114 | req.ignore404 = True
115 |
116 | self.assertIsInstance(req, HTTPRequest)
117 |
118 | self.assertEqual(req.method, "method test")
119 | self.assertEqual(req.resource, "resource test")
120 | self.assertEqual(req.headers, {"test": "val"})
121 | self.assertEqual(req.query_params, {"test": "val"})
122 | self.assertEqual(req.content, "content test")
123 | self.assertTrue(req.ignore404)
124 |
125 | def test_setters_none(self):
126 | req = HTTPRequest()
127 |
128 | req.method = None
129 | req.resource = None
130 | req.headers = None
131 | req.query_params = None
132 | req.content = None
133 | req.ignore404 = None
134 |
135 | self.assertIsInstance(req, HTTPRequest)
136 |
137 | self.assertTrue(hasattr(req, "content"))
138 | self.assertIsNone(req.content)
139 |
140 | self.assertTrue(hasattr(req, "ignore404"))
141 | self.assertFalse(req.ignore404)
142 |
143 | self.assertTrue(hasattr(req, "method"))
144 | self.assertIsNone(req.method)
145 |
146 | self.assertTrue(hasattr(req, "resource"))
147 | self.assertIsNone(req.resource)
148 |
149 | self.assertTrue(hasattr(req, "headers"))
150 | self.assertEqual(req.headers, {})
151 |
152 | self.assertTrue(hasattr(req, "query_params"))
153 | self.assertEqual(req.query_params, {})
154 |
155 | def test_headers_setter(self):
156 | class Tester(object):
157 | def __init__(self, param="ok", tester="test"):
158 | self.param = param
159 | self.tester = tester
160 |
161 | obj = Tester()
162 | req = HTTPRequest(headers=obj)
163 |
164 | self.assertIsInstance(req, HTTPRequest)
165 | self.assertIsInstance(req.headers, dict)
166 | self.assertTrue("param" in req.headers)
167 | self.assertEqual(req.headers["param"], "ok")
168 | self.assertTrue("tester" in req.headers)
169 | self.assertEqual(req.headers["tester"], "test")
170 |
171 | def test_query_params_setter(self):
172 | class Tester(object):
173 | def __init__(self, param="ok", tester="test"):
174 | self.param = param
175 | self.tester = tester
176 |
177 | obj = Tester()
178 | req = HTTPRequest(query_params=obj)
179 |
180 | self.assertIsInstance(req, HTTPRequest)
181 | self.assertIsInstance(req.query_params, dict)
182 | self.assertTrue("param" in req.query_params)
183 | self.assertEqual(req.query_params["param"], "ok")
184 | self.assertTrue("tester" in req.query_params)
185 | self.assertEqual(req.query_params["tester"], "test")
186 |
187 |
188 | if __name__ == "__main__":
189 | suite = unittest.TestLoader().loadTestsFromTestCase(HTTPRequestTest)
190 | unittest.TextTestRunner(verbosity=2).run(suite)
191 |
--------------------------------------------------------------------------------
/test/interactioncomponent_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | import json
15 | import unittest
16 |
17 | if __name__ == '__main__':
18 | from test.main import setup_tincan_path
19 |
20 | setup_tincan_path()
21 | from tincan import InteractionComponent, LanguageMap
22 |
23 |
24 | class InteractionComponentTest(unittest.TestCase):
25 | def test_InitEmpty(self):
26 | icomp = InteractionComponent()
27 | self.assertIsNone(icomp.id)
28 | self.assertNotIn('description', vars(icomp))
29 |
30 | def test_InitExceptionEmptyId(self):
31 | with self.assertRaises(ValueError):
32 | InteractionComponent(id='')
33 |
34 | def test_InitId(self):
35 | icomp = InteractionComponent(id='test')
36 | self.assertEqual(icomp.id, 'test')
37 | self.assertNotIn('description', vars(icomp))
38 |
39 | def test_InitDescription(self):
40 | icomp = InteractionComponent(description={"en-US": "test"})
41 | self.assertIsNone(icomp.id)
42 | self.descriptionVerificationHelper(icomp.description)
43 |
44 | def test_InitEmptyDescription(self):
45 | icomp = InteractionComponent(id='test', description={})
46 | self.assertEqual(icomp.id, 'test')
47 | self.assertIsInstance(icomp.description, LanguageMap)
48 | self.assertEqual(len(vars(icomp.description)), 0)
49 |
50 | def test_InitAnonDescription(self):
51 | icomp = InteractionComponent(id='test', description={"en-US": "test"})
52 | self.assertEqual(icomp.id, 'test')
53 | self.descriptionVerificationHelper(icomp.description)
54 |
55 | def test_InitLanguageMapDescription(self):
56 | icomp = InteractionComponent(id='test', description=LanguageMap({"en-US": "test"}))
57 | self.assertEqual(icomp.id, 'test')
58 | self.descriptionVerificationHelper(icomp.description)
59 |
60 | def test_InitEmptyLanguageMapDescription(self):
61 | icomp = InteractionComponent(id='test', description=LanguageMap({}))
62 | self.assertEqual(icomp.id, 'test')
63 | self.assertIsInstance(icomp.description, LanguageMap)
64 | self.assertEqual(len(vars(icomp.description)), 0)
65 |
66 | def test_InitUnpackDescription(self):
67 | obj = {"description": {"en-US": "test"}}
68 | icomp = InteractionComponent(**obj)
69 | self.descriptionVerificationHelper(icomp.description)
70 |
71 | def test_InitUnpack(self):
72 | obj = {"id": "test", "description": {"en-US": "test"}}
73 | icomp = InteractionComponent(**obj)
74 | self.assertEqual(icomp.id, 'test')
75 | self.descriptionVerificationHelper(icomp.description)
76 |
77 | def test_InitExceptionUnpackEmptyId(self):
78 | obj = {"id": ""}
79 | with self.assertRaises(ValueError):
80 | InteractionComponent(**obj)
81 |
82 | def test_InitExceptionUnpackFlatDescription(self):
83 | obj = {"id": "test", "description": "test"}
84 | with self.assertRaises(ValueError):
85 | InteractionComponent(**obj)
86 |
87 | def test_FromJSONExceptionBadJSON(self):
88 | with self.assertRaises(ValueError):
89 | InteractionComponent.from_json('{"bad JSON"}')
90 |
91 | def test_FromJSONExceptionMalformedJSON(self):
92 | with self.assertRaises(AttributeError):
93 | InteractionComponent.from_json('{"test": "invalid property"}')
94 |
95 | """ An exception is best here to keep client code from thinking its doing \
96 | something its not when instantiating a InteractionComponent """
97 |
98 | def test_FromJSONExceptionPartiallyMalformedJSON(self):
99 | with self.assertRaises(AttributeError):
100 | InteractionComponent.from_json('{"test": "invalid property", "id": \
101 | "valid property"}')
102 |
103 | def test_FromJSONEmptyObject(self):
104 | icomp = InteractionComponent.from_json('{}')
105 | self.assertIsNone(icomp.id)
106 | self.assertNotIn('description', vars(icomp))
107 |
108 | def test_FromJSONExceptionEmpty(self):
109 | with self.assertRaises(ValueError):
110 | InteractionComponent.from_json('')
111 |
112 | def test_FromJSONId(self):
113 | icomp = InteractionComponent.from_json('{"id": "test"}')
114 | self.assertEqual(icomp.id, 'test')
115 | self.assertNotIn('description', vars(icomp))
116 |
117 | def test_FromJSONExceptionFlatDescription(self):
118 | with self.assertRaises(ValueError):
119 | InteractionComponent.from_json('{"id": "test", "description": "flatdescription"}')
120 |
121 | def test_FromJSON(self):
122 | icomp = InteractionComponent.from_json('{"id": "test", "description": {"en-US": "test"}}')
123 | self.assertEqual(icomp.id, 'test')
124 | self.descriptionVerificationHelper(icomp.description)
125 |
126 | def test_AsVersionEmpty(self):
127 | icomp = InteractionComponent()
128 | icomp2 = icomp.as_version("1.0.0")
129 | self.assertEqual(icomp2, {})
130 |
131 | def test_AsVersionNotEmpty(self):
132 | icomp = InteractionComponent(**{'id': 'test'})
133 | icomp2 = icomp.as_version()
134 | self.assertEqual(icomp2, {'id': 'test'})
135 |
136 | def test_ToJSONFromJSON(self):
137 | json_str = '{"id": "test", "description": {"en-US": "test"}}'
138 | icomp = InteractionComponent.from_json(json_str)
139 | self.assertEqual(icomp.id, 'test')
140 | self.descriptionVerificationHelper(icomp.description)
141 | self.assertEqual(json.loads(icomp.to_json()), json.loads(json_str))
142 |
143 | def test_ToJSON(self):
144 | icomp = InteractionComponent(**{"id": "test", "description": {"en-US": "test"}})
145 | self.assertEqual(json.loads(icomp.to_json()), json.loads('{"id": "test", "description": {"en-US": "test"}}'))
146 |
147 | def test_ToJSONIgnoreNoneDescription(self):
148 | icomp = InteractionComponent(id='test')
149 | self.assertEqual(icomp.to_json(), '{"id": "test"}')
150 |
151 | def test_ToJSONIgnoreNoneId(self):
152 | icomp = InteractionComponent(description={"en-US": "test"})
153 | self.assertEqual(icomp.to_json(), '{"description": {"en-US": "test"}}')
154 |
155 | def test_ToJSONEmpty(self):
156 | icomp = InteractionComponent()
157 | self.assertEqual(icomp.to_json(), '{}')
158 |
159 | def descriptionVerificationHelper(self, description):
160 | self.assertIsInstance(description, LanguageMap)
161 | self.assertEqual(len(description), 1)
162 | self.assertIn('en-US', description)
163 | self.assertEqual(description['en-US'], 'test')
164 |
165 |
166 | if __name__ == '__main__':
167 | suite = unittest.TestLoader().loadTestsFromTestCase(InteractionComponentTest)
168 | unittest.TextTestRunner(verbosity=2).run(suite)
169 |
--------------------------------------------------------------------------------
/test/languagemap_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | import json
15 | import unittest
16 |
17 | if __name__ == '__main__':
18 | from test.main import setup_tincan_path
19 |
20 | setup_tincan_path()
21 | from tincan import LanguageMap
22 |
23 |
24 | class LanguageMapTest(unittest.TestCase):
25 | def test_InitNoArgs(self):
26 | lmap = LanguageMap()
27 | self.assertEqual(lmap, {})
28 | self.assertIsInstance(lmap, LanguageMap)
29 |
30 | def test_InitEmpty(self):
31 | lmap = LanguageMap({})
32 | self.assertEqual(lmap, {})
33 | self.assertIsInstance(lmap, LanguageMap)
34 |
35 | def test_InitExceptionNotMap(self):
36 | with self.assertRaises(ValueError):
37 | LanguageMap('not map')
38 |
39 | def test_InitExceptionBadMap(self):
40 | with self.assertRaises(ValueError):
41 | LanguageMap({"bad map"})
42 |
43 | def test_InitExceptionNestedObject(self):
44 | with self.assertRaises(TypeError):
45 | LanguageMap({"en-US": {"nested": "object"}})
46 |
47 | def test_InitDict(self):
48 | lmap = LanguageMap({"en-US": "US-test", "fr-CA": "CA-test", "fr-FR": "FR-test"})
49 | self.mapVerificationHelper(lmap)
50 |
51 | def test_InitLanguageMap(self):
52 | arg = LanguageMap({"en-US": "US-test", "fr-CA": "CA-test", "fr-FR": "FR-test"})
53 | lmap = LanguageMap(arg)
54 | self.mapVerificationHelper(lmap)
55 |
56 | def test_InitUnpack(self):
57 | obj = {"en-US": "US-test", "fr-CA": "CA-test", "fr-FR": "FR-test"}
58 | lmap = LanguageMap(**obj)
59 | self.mapVerificationHelper(lmap)
60 |
61 | def test_InitUnpackExceptionNestedObject(self):
62 | obj = {"en-US": {"nested": "object"}}
63 | with self.assertRaises(TypeError):
64 | LanguageMap(**obj)
65 |
66 | def test_FromJSON(self):
67 | lmap = LanguageMap.from_json('{"en-US": "US-test", "fr-CA": "CA-test", "fr-FR": "FR-test"}')
68 | self.mapVerificationHelper(lmap)
69 |
70 | def test_FromJSONExceptionBadJSON(self):
71 | with self.assertRaises(ValueError):
72 | LanguageMap.from_json('{"bad JSON"}')
73 |
74 | def test_FromJSONExceptionNestedObject(self):
75 | with self.assertRaises(TypeError):
76 | LanguageMap.from_json('{"fr-CA": "test", "en-US": {"nested": "object"}}')
77 |
78 | def test_FromJSONEmptyObject(self):
79 | lmap = LanguageMap.from_json('{}')
80 | self.assertIsInstance(lmap, LanguageMap)
81 | self.assertEqual(lmap, {})
82 |
83 | def test_AsVersionEmpty(self):
84 | lmap = LanguageMap()
85 | check = lmap.as_version()
86 | self.assertEqual(check, {})
87 |
88 | def test_AsVersionNotEmpty(self):
89 | lmap = LanguageMap({"en-US": "US-test", "fr-CA": "CA-test", "fr-FR": "FR-test"})
90 | check = lmap.as_version()
91 | self.assertEqual(check, {"en-US": "US-test", "fr-CA": "CA-test", "fr-FR": "FR-test"})
92 |
93 | def test_ToJSONFromJSON(self):
94 | json_str = '{"fr-CA": "CA-test", "en-US": "US-test", "fr-FR": "FR-test"}'
95 | lmap = LanguageMap.from_json(json_str)
96 | self.mapVerificationHelper(lmap)
97 | self.assertEqual(json.loads(lmap.to_json()), json.loads(json_str))
98 |
99 | def test_ToJSON(self):
100 | lmap = LanguageMap({"en-US": "US-test", "fr-CA": "CA-test", "fr-FR": "FR-test"})
101 | # since the map is unordered, it is ok that to_json() changes ordering
102 | self.assertEqual(json.loads(lmap.to_json()),
103 | json.loads('{"fr-CA": "CA-test", "en-US": "US-test", "fr-FR": "FR-test"}'))
104 |
105 | def test_getItemException(self):
106 | lmap = LanguageMap()
107 | with self.assertRaises(KeyError):
108 | _ = lmap['en-Anything']
109 |
110 | def test_setItem(self):
111 | lmap = LanguageMap()
112 | lmap['en-US'] = 'US-test'
113 | lmap['fr-CA'] = 'CA-test'
114 | lmap['fr-FR'] = 'FR-test'
115 | self.mapVerificationHelper(lmap)
116 |
117 | def test_setItemException(self):
118 | lmap = LanguageMap()
119 | with self.assertRaises(TypeError):
120 | lmap['en-US'] = {"test": "notstring"}
121 | self.assertEqual(lmap, {})
122 |
123 | def mapVerificationHelper(self, lmap):
124 | self.assertIsInstance(lmap, LanguageMap)
125 | self.assertEqual(len(lmap), 3)
126 | self.assertIn('en-US', lmap)
127 | self.assertIn('fr-CA', lmap)
128 | self.assertIn('fr-FR', lmap)
129 | self.assertEqual(lmap['en-US'], 'US-test')
130 | self.assertEqual(lmap['fr-CA'], 'CA-test')
131 | self.assertEqual(lmap['fr-FR'], 'FR-test')
132 |
133 |
134 | if __name__ == '__main__':
135 | suite = unittest.TestLoader().loadTestsFromTestCase(LanguageMapTest)
136 | unittest.TextTestRunner(verbosity=2).run(suite)
137 |
--------------------------------------------------------------------------------
/test/lrs_response_test.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 | #
3 | # Copyright 2014 Rustici Software
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless respuired by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | import unittest
18 | import http.client
19 |
20 | if __name__ == '__main__':
21 | from test.main import setup_tincan_path
22 |
23 | setup_tincan_path()
24 | from tincan import LRSResponse, HTTPRequest
25 |
26 |
27 | class LRSResponseTest(unittest.TestCase):
28 | def setUp(self):
29 | pass
30 |
31 | def tearDown(self):
32 | pass
33 |
34 | def test_init_empty(self):
35 | resp = LRSResponse()
36 | self.assertIsInstance(resp, LRSResponse)
37 | self.assertIsNone(resp.content)
38 |
39 | self.assertTrue(hasattr(resp, "success"))
40 | self.assertFalse(resp.success)
41 |
42 | self.assertTrue(hasattr(resp, "request"))
43 | self.assertIsNone(resp.request)
44 |
45 | self.assertTrue(hasattr(resp, "response"))
46 | self.assertIsNone(resp.response)
47 |
48 | def test_init_kwarg_exception(self):
49 | with self.assertRaises(AttributeError):
50 | LRSResponse(bad_test="test")
51 |
52 | def test_init_arg_exception_dict(self):
53 | d = {"bad_test": "test", "content": "ok"}
54 | with self.assertRaises(AttributeError):
55 | LRSResponse(d)
56 |
57 | def test_init_arg_exception_obj(self):
58 | class Tester(object):
59 | def __init__(self, success=True, bad_test="test"):
60 | self.success = success
61 | self.bad_test = bad_test
62 |
63 | obj = Tester()
64 |
65 | with self.assertRaises(AttributeError):
66 | LRSResponse(obj)
67 |
68 | def test_init_partial(self):
69 | req = HTTPRequest(resource="test")
70 |
71 | resp = LRSResponse(
72 | success=True,
73 | content="content test",
74 | request=req,
75 | )
76 | self.assertIsInstance(resp, LRSResponse)
77 |
78 | self.assertTrue(resp.success)
79 | self.assertEqual(resp.content, "content test")
80 | self.assertIsInstance(resp.request, HTTPRequest)
81 | self.assertEqual(resp.request, req)
82 |
83 | self.assertTrue(hasattr(resp, "response"))
84 | self.assertIsNone(resp.response)
85 |
86 | def test_init_all(self):
87 | conn = http.client.HTTPConnection("tincanapi.com")
88 | conn.request("GET", "")
89 | web_resp = conn.getresponse()
90 |
91 | req = HTTPRequest(resource="test")
92 |
93 | resp = LRSResponse(
94 | success=True,
95 | content="content test",
96 | request=req,
97 | response=web_resp,
98 |
99 | )
100 | self.assertIsInstance(resp, LRSResponse)
101 |
102 | self.assertTrue(resp.success)
103 | self.assertEqual(resp.content, "content test")
104 | self.assertIsInstance(resp.request, HTTPRequest)
105 | self.assertEqual(resp.request, req)
106 |
107 | self.assertIsInstance(resp.response, http.client.HTTPResponse)
108 | self.assertEqual(resp.response, web_resp)
109 |
110 | def test_setters(self):
111 | conn = http.client.HTTPConnection("tincanapi.com")
112 | conn.request("GET", "")
113 | web_resp = conn.getresponse()
114 |
115 | req = HTTPRequest(resource="test")
116 |
117 | resp = LRSResponse()
118 | resp.success = True
119 | resp.content = "content test"
120 | resp.request = req
121 | resp.response = web_resp
122 |
123 | self.assertIsInstance(resp, LRSResponse)
124 |
125 | self.assertTrue(resp.success)
126 | self.assertEqual(resp.content, "content test")
127 | self.assertIsInstance(resp.request, HTTPRequest)
128 | self.assertEqual(resp.request, req)
129 | self.assertEqual(resp.request.resource, "test")
130 |
131 | self.assertIsInstance(resp.response, http.client.HTTPResponse)
132 | self.assertEqual(resp.response, web_resp)
133 |
134 | def test_unicode(self):
135 | resp = LRSResponse()
136 | resp.data = b"\xce\xb4\xce\xbf\xce\xba\xce\xb9\xce\xbc\xce\xae " \
137 | b"\xcf\x80\xce\xb5\xcf\x81\xce\xb9\xce\xb5\xcf\x87" \
138 | b"\xce\xbf\xce\xbc\xce\xad\xce\xbd\xce\xbf\xcf\x85"
139 |
140 | self.assertIsInstance(resp, LRSResponse)
141 | self.assertIsInstance(resp.data, str)
142 | self.assertEqual(resp.data, u"δοκιμή περιεχομένου")
143 |
144 | def test_setters_none(self):
145 | resp = LRSResponse()
146 |
147 | resp.success = None
148 | resp.content = None
149 | resp.request = None
150 | resp.response = None
151 |
152 | self.assertIsInstance(resp, LRSResponse)
153 |
154 | self.assertTrue(hasattr(resp, "content"))
155 | self.assertIsNone(resp.content)
156 |
157 | self.assertTrue(hasattr(resp, "success"))
158 | self.assertFalse(resp.success)
159 |
160 | self.assertTrue(hasattr(resp, "request"))
161 | self.assertIsNone(resp.request)
162 |
163 | self.assertTrue(hasattr(resp, "response"))
164 | self.assertIsNone(resp.response)
165 |
166 | def test_request_setter(self):
167 | class Tester(object):
168 | def __init__(self, resource="ok", headers=None):
169 | if headers is None:
170 | headers = {"test": "ok"}
171 |
172 | self.resource = resource
173 | self.headers = headers
174 |
175 | obj = Tester()
176 |
177 | resp = LRSResponse(request=obj)
178 |
179 | self.assertIsInstance(resp, LRSResponse)
180 | self.assertIsInstance(resp.request, HTTPRequest)
181 | self.assertTrue(hasattr(resp.request, "resource"))
182 | self.assertEqual(resp.request.resource, "ok")
183 | self.assertTrue(hasattr(resp.request, "headers"))
184 | self.assertEqual(resp.request.headers, {"test": "ok"})
185 |
186 | def test_response_setter(self):
187 | class Tester(object):
188 | def __init__(self, msg="ok", version="test"):
189 | self.msg = msg
190 | self.version = version
191 |
192 | obj = Tester()
193 | with self.assertRaises(TypeError):
194 | LRSResponse(response=obj)
195 |
196 |
197 | if __name__ == "__main__":
198 | suite = unittest.TestLoader().loadTestsFromTestCase(LRSResponseTest)
199 | unittest.TextTestRunner(verbosity=2).run(suite)
200 |
--------------------------------------------------------------------------------
/test/main.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """
16 | Discovers and runs all tests in the test module.
17 |
18 | All tests must end in '_test.py' to be included in the test. It is
19 | highly recommended that tests be named in the format
20 | '_test.py'.
21 | """
22 | import unittest
23 | import sys
24 | import os
25 | from os.path import dirname, join as path_join
26 |
27 |
28 | def add_tincan_to_path(dirpath):
29 | tincan_path = path_join(dirpath, 'tincan')
30 | if os.path.isdir(tincan_path) and dirpath not in sys.path:
31 | sys.path.append(dirpath)
32 | return True
33 | return False
34 |
35 |
36 | def check_tincan_importability():
37 | try:
38 | import tincan
39 | except ImportError as e:
40 | tincan = None
41 | raise ImportError(
42 | f"Could not import tincan."
43 | f"\n\n"
44 | f"This probably means that the test directory is not a "
45 | f"sibling directory of tincan. Move test and tincan into "
46 | f"the same folder and try again."
47 | f"\n\n {repr(e)}"
48 | )
49 |
50 |
51 | def locate_package(pkg):
52 | this_file = os.path.abspath(__file__)
53 | path = this_file
54 | while not os.path.isdir(os.path.join(path, pkg)):
55 | path = dirname(path)
56 | if not path or dirname(path) == path:
57 | return False
58 | return path
59 |
60 |
61 | def setup_tincan_path():
62 | tincan_pardir = locate_package('tincan')
63 | if tincan_pardir and tincan_pardir not in sys.path:
64 | #
65 | # using sys.path.insert in this manner is considered a little evil,
66 | # see http://stackoverflow.com/questions/10095037/why-use-sys-path-appendpath-instead-of-sys-path-insert1-path
67 | # but this is better than using the sys.path.append as before
68 | # as that was catching the system installed version, if virtualenv
69 | # is ever implemented for the test suite then this can go away
70 | #
71 | print(f"Adding {repr(tincan_pardir)} to PYTHONPATH")
72 | sys.path.insert(1, tincan_pardir)
73 | check_tincan_importability()
74 |
75 |
76 | if __name__ == '__main__':
77 | setup_tincan_path()
78 |
79 | loader = unittest.TestLoader()
80 | test_pardir = locate_package('test')
81 | test_dir = os.path.join(test_pardir, 'test')
82 | suite = loader.discover(test_dir, pattern='*_test.py')
83 | ret = not unittest.TextTestRunner(verbosity=1).run(suite).wasSuccessful()
84 | sys.exit(ret)
85 |
--------------------------------------------------------------------------------
/test/resources/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RusticiSoftware/TinCanPython/bbc3f9dd5d8385e7b66c693e7f8262561392be74/test/resources/__init__.py
--------------------------------------------------------------------------------
/test/resources/lrs_properties.py.template:
--------------------------------------------------------------------------------
1 | """
2 | Contains user-specific information for testing.
3 | """
4 |
5 |
6 | endpoint = ""
7 | version = "" # 1.0.1 | 1.0.0 | 0.95 | 0.9
8 | username = ""
9 | password = ""
10 |
--------------------------------------------------------------------------------
/test/resources/lrs_properties.py.travis-ci:
--------------------------------------------------------------------------------
1 | """
2 | Contains configuration specific to Travis CI testing
3 | """
4 |
5 |
6 | endpoint="https://cloud.scorm.com/tc/RQVHO4MI7J/sandbox/"
7 | version="1.0.1"
8 | username="VaNxecRiU3pv3WG5Ouw"
9 | password="m7Uwk71z7PJfbzQCWcU"
10 |
--------------------------------------------------------------------------------
/test/result_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import unittest
16 | from datetime import timedelta
17 |
18 | if __name__ == '__main__':
19 | from test.main import setup_tincan_path
20 |
21 | setup_tincan_path()
22 | from tincan import Score, Extensions, Result
23 | from test.test_utils import TinCanBaseTestCase
24 |
25 |
26 | class ResultTest(TinCanBaseTestCase):
27 | def setUp(self):
28 | self.score = Score(max=100.0, min=0.0, raw=80.0, scaled=0.8)
29 | self.extensions = Extensions({'http://example.com/extension': 'extension value', })
30 |
31 | def test_serialize_deserialize(self):
32 | res = Result()
33 | res.completion = True
34 | res.duration = timedelta(seconds=1.75)
35 | # res.duration = 'PT1.75S' # ISO 8601
36 | res.extensions = self.extensions
37 | res.response = "Here's a response"
38 | res.score = self.score
39 | res.success = False
40 |
41 | self.assertSerializeDeserialize(res)
42 |
43 | def test_serialize_deserialize_init(self):
44 | data = {
45 | 'completion': True,
46 | 'duration': timedelta(seconds=1.75),
47 | # 'duration': 'PT1.75S', # ISO 8601
48 | 'extensions': self.extensions,
49 | 'response': "Here's a response",
50 | 'score': self.score,
51 | 'success': False,
52 | }
53 | res = Result(**data)
54 |
55 | self.assertSerializeDeserialize(res)
56 |
57 | def test_bad_property_init(self):
58 | with self.assertRaises(AttributeError):
59 | Result(bad_name=2)
60 |
61 | with self.assertRaises(AttributeError):
62 | Result({'bad_name': 2})
63 |
64 |
65 | if __name__ == '__main__':
66 | suite = unittest.TestLoader().loadTestsFromTestCase(ResultTest)
67 | unittest.TextTestRunner(verbosity=2).run(suite)
68 |
--------------------------------------------------------------------------------
/test/score_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import unittest
16 |
17 | if __name__ == '__main__':
18 | from test.main import setup_tincan_path
19 |
20 | setup_tincan_path()
21 | from tincan import Score
22 | from test.test_utils import TinCanBaseTestCase
23 |
24 |
25 | class ScoreTest(TinCanBaseTestCase):
26 | def test_serialize_deserialize(self):
27 | s = Score()
28 | s.max = 100.0
29 | s.min = 0.0
30 | s.raw = 80.0
31 | s.scaled = 0.8
32 |
33 | self.assertSerializeDeserialize(s)
34 |
35 | def test_serialize_deserialize_init(self):
36 | s = Score(max=100.0, min=0.0, raw=80.0, scaled=0.8)
37 |
38 | self.assertSerializeDeserialize(s)
39 |
40 | def test_bad_property_init(self):
41 | with self.assertRaises(AttributeError):
42 | Score(bad_name=2)
43 |
44 | with self.assertRaises(AttributeError):
45 | Score({'bad_name': 2})
46 |
47 |
48 | if __name__ == '__main__':
49 | suite = unittest.TestLoader().loadTestsFromTestCase(ScoreTest)
50 | unittest.TextTestRunner(verbosity=2).run(suite)
51 |
--------------------------------------------------------------------------------
/test/statementref_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | import json
15 | import unittest
16 | import uuid
17 |
18 | if __name__ == '__main__':
19 | from test.main import setup_tincan_path
20 |
21 | setup_tincan_path()
22 | from tincan import StatementRef
23 |
24 |
25 | class StatementRefTest(unittest.TestCase):
26 | def test_InitObjectType(self):
27 | statementref = StatementRef(object_type='StatementRef')
28 | self.assertEqual(statementref.object_type, 'StatementRef')
29 |
30 | def test_InitId(self):
31 | statementref = StatementRef(id='016699c6-d600-48a7-96ab-86187498f16f')
32 | self.assertEqual(statementref.id, uuid.UUID('016699c6-d600-48a7-96ab-86187498f16f'))
33 |
34 | def test_InitUnpack(self):
35 | obj = {'object_type': 'StatementRef', 'id': '016699c6-d600-48a7-96ab-86187498f16f'}
36 | statementref = StatementRef(**obj)
37 | self.assertEqual(statementref.object_type, 'StatementRef')
38 | self.assertEqual(statementref.id, uuid.UUID('016699c6-d600-48a7-96ab-86187498f16f'))
39 |
40 | def test_FromJSON(self):
41 | json_str = '{"object_type":"StatementRef", "id":"016699c6-d600-48a7-96ab-86187498f16f"}'
42 | statementref = StatementRef.from_json(json_str)
43 | self.assertEqual(statementref.object_type, 'StatementRef')
44 | self.assertEqual(statementref.id, uuid.UUID('016699c6-d600-48a7-96ab-86187498f16f'))
45 |
46 | def test_ToJSON(self):
47 | statementref = StatementRef(object_type='StatementRef', id='016699c6-d600-48a7-96ab-86187498f16f')
48 | self.assertEqual(json.loads(statementref.to_json()),
49 | json.loads('{"id": "016699c6-d600-48a7-96ab-86187498f16f", "objectType": "StatementRef"}'))
50 |
51 | def test_ToJSONNoObjectType(self):
52 | statementref = StatementRef(id='016699c6-d600-48a7-96ab-86187498f16f')
53 | self.assertEqual(json.loads(statementref.to_json()),
54 | json.loads('{"id": "016699c6-d600-48a7-96ab-86187498f16f", "objectType": "StatementRef"}'))
55 |
56 | def test_FromJSONToJSON(self):
57 | json_str = '{"object_type":"StatementRef", "id":"016699c6-d600-48a7-96ab-86187498f16f"}'
58 | statementref = StatementRef.from_json(json_str)
59 | self.assertEqual(statementref.object_type, 'StatementRef')
60 | self.assertEqual(statementref.id, uuid.UUID('016699c6-d600-48a7-96ab-86187498f16f'))
61 | self.assertEqual(json.loads(statementref.to_json()),
62 | json.loads('{"id": "016699c6-d600-48a7-96ab-86187498f16f", "objectType": "StatementRef"}'))
63 |
64 | def test_ToJSONEmpty(self):
65 | statementref = StatementRef()
66 | self.assertEqual(statementref.to_json(), '{"objectType": "StatementRef"}')
67 |
68 | def test_ExceptionInvalidUUID(self):
69 | with self.assertRaises(ValueError):
70 | StatementRef(id='badtest')
71 |
72 |
73 | if __name__ == '__main__':
74 | suite = unittest.TestLoader().loadTestsFromTestCase(StatementRefTest)
75 | unittest.TextTestRunner(verbosity=2).run(suite)
76 |
--------------------------------------------------------------------------------
/test/statements_result_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import unittest
16 | import uuid
17 |
18 | if __name__ == '__main__':
19 | from test.main import setup_tincan_path
20 |
21 | setup_tincan_path()
22 | from tincan import StatementsResult, Statement
23 | from test.test_utils import TinCanBaseTestCase
24 |
25 |
26 | class StatementsResultTest(TinCanBaseTestCase):
27 | def test_serialize_deserialize(self):
28 | sr = StatementsResult()
29 | uuid_str = '016699c6-d600-48a7-96ab-86187498f16f'
30 | sr.statements = [Statement(id=uuid_str), Statement(id=uuid_str), Statement(id=uuid_str), ]
31 | sr.more = 'http://www.example.com/more/1234'
32 |
33 | self.assertSerializeDeserialize(sr)
34 |
35 | def test_serialize_deserialize_init(self):
36 | uuid_str = '016699c6-d600-48a7-96ab-86187498f16f'
37 | data = {
38 | 'statements': [Statement(id=uuid_str), Statement(id=uuid_str), Statement(id=uuid_str),
39 | Statement(id=uuid_str), ],
40 | 'more': 'http://www.example.com/more/1234',
41 | }
42 |
43 | sr = StatementsResult(data)
44 | self.assertSerializeDeserialize(sr)
45 |
46 | def test_read_write(self):
47 | sr = StatementsResult()
48 | self.assertEqual(len(sr.statements), 0, 'Empty StatementsResult inited as non-empty!')
49 |
50 | uuid_str = '016699c6-d600-48a7-96ab-86187498f16f'
51 | sr.statements = (Statement(id=uuid_str), Statement(id=uuid_str), Statement(id=uuid_str),)
52 | self.assertIsInstance(sr.statements, list, 'Did not convert tuple to list!')
53 |
54 | sr.statements.append(Statement(id=uuid_str))
55 | self.assertEqual(sr.statements[3].id, uuid.UUID('016699c6-d600-48a7-96ab-86187498f16f'),
56 | 'Did not append value!')
57 |
58 | self.assertIsNone(sr.more)
59 |
60 | sr.more = 'http://www.example.com/more/1234'
61 |
62 | self.assertEqual(sr.more, 'http://www.example.com/more/1234', 'Did not set sr.more!')
63 |
64 |
65 | if __name__ == '__main__':
66 | suite = unittest.TestLoader().loadTestsFromTestCase(StatementsResultTest)
67 | unittest.TextTestRunner(verbosity=2).run(suite)
68 |
--------------------------------------------------------------------------------
/test/substatement_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | import json
15 | import unittest
16 |
17 | if __name__ == '__main__':
18 | from test.main import setup_tincan_path
19 |
20 | setup_tincan_path()
21 | from tincan import (
22 | Agent,
23 | Group,
24 | Verb,
25 | StatementRef,
26 | Activity,
27 | SubStatement,
28 | )
29 |
30 |
31 | class SubStatementTest(unittest.TestCase):
32 | def test_InitAnonAgentActor(self):
33 | substatement = SubStatement(actor={'object_type': 'Agent', 'name': 'test'})
34 | self.agentVerificationHelper(substatement.actor)
35 |
36 | def test_InitAnonGroupActor(self):
37 | substatement = SubStatement(actor={'object_type': 'Group', 'member': [{"name": "test"}]})
38 | self.groupVerificationHelper(substatement.actor)
39 |
40 | def test_InitAnonVerb(self):
41 | substatement = SubStatement(verb={'id': 'test'})
42 | self.verbVerificationHelper(substatement.verb)
43 |
44 | def test_InitAnonObject(self):
45 | substatement = SubStatement(object={'id': 'test'})
46 | self.activityVerificationHelper(substatement.object)
47 |
48 | def test_InitAnonAgentObject(self):
49 | substatement = SubStatement(object={'object_type': 'Agent', 'name': 'test'})
50 | self.agentVerificationHelper(substatement.object)
51 |
52 | def test_InitDifferentNamingObject(self):
53 | substatement = SubStatement(object={'objectType': 'Agent', 'name': 'test'})
54 | self.agentVerificationHelper(substatement.object)
55 |
56 | def test_InitObjectType(self):
57 | substatement = SubStatement(object_type="SubStatement")
58 | self.assertEqual(substatement.object_type, "SubStatement")
59 |
60 | def test_InitAgentActor(self):
61 | substatement = SubStatement(actor=Agent(name='test'))
62 | self.agentVerificationHelper(substatement.actor)
63 |
64 | def test_InitGroupActor(self):
65 | substatement = SubStatement(actor=Group(member=[Agent(name='test')]))
66 | self.groupVerificationHelper(substatement.actor)
67 |
68 | def test_InitVerb(self):
69 | substatement = SubStatement(verb=Verb(id='test'))
70 | self.verbVerificationHelper(substatement.verb)
71 |
72 | def test_InitAgentObject(self):
73 | substatement = SubStatement(object=Agent(name='test'))
74 | self.agentVerificationHelper(substatement.object)
75 |
76 | def test_InitGroupObject(self):
77 | substatement = SubStatement(object=Group(member=[Agent(name='test')]))
78 | self.groupVerificationHelper(substatement.object)
79 |
80 | def test_InitActivityObject(self):
81 | substatement = SubStatement(object=Activity(id='test'))
82 | self.activityVerificationHelper(substatement.object)
83 |
84 | def test_InitUnpack(self):
85 | obj = {'object_type': 'SubStatement', 'actor': {'name': 'test'}, 'verb': {'id': 'test'},
86 | 'object': {'id': 'test'}}
87 | substatement = SubStatement(**obj)
88 | self.assertEqual(substatement.object_type, 'SubStatement')
89 | self.agentVerificationHelper(substatement.actor)
90 | self.verbVerificationHelper(substatement.verb)
91 | self.activityVerificationHelper(substatement.object)
92 |
93 | def test_FromJSON(self):
94 | json_str = '{"object_type":"SubStatement", "actor":{"name":"test"}, ' \
95 | '"verb":{"id":"test"}, "object":{"id":"test"}}'
96 | substatement = SubStatement.from_json(json_str)
97 | self.assertEqual(substatement.object_type, 'SubStatement')
98 | self.agentVerificationHelper(substatement.actor)
99 | self.verbVerificationHelper(substatement.verb)
100 | self.activityVerificationHelper(substatement.object)
101 |
102 | def test_ToJSONEmpty(self):
103 | substatement = SubStatement()
104 | self.assertEqual(json.loads(substatement.to_json()), json.loads('{"objectType": "SubStatement"}'))
105 |
106 | def test_ToJSON(self):
107 | substatement = SubStatement(object_type='SubStatement', actor=Agent(name='test'), verb=Verb(id='test'),
108 | object=Activity(id='test'))
109 | self.assertEqual(json.loads(substatement.to_json()),
110 | json.loads('{"verb": {"id": "test"}, "object": {"id": "test", "objectType": "Activity"}, '
111 | '"actor": {"name": "test", "objectType": "Agent"}, "objectType": "SubStatement"}'))
112 |
113 | def test_FromJSONToJSON(self):
114 | json_str = '{"object_type":"SubStatement", "actor":{"name":"test"}, "verb":{"id":"test"}, "' \
115 | 'object":{"id":"test", "objectType": "Activity"}}'
116 | substatement = SubStatement.from_json(json_str)
117 | self.assertEqual(substatement.object_type, 'SubStatement')
118 | self.agentVerificationHelper(substatement.actor)
119 | self.verbVerificationHelper(substatement.verb)
120 | self.activityVerificationHelper(substatement.object)
121 | self.assertEqual(json.loads(substatement.to_json()),
122 | json.loads('{"verb": {"id": "test"}, "object": {"id": "test", "objectType": "Activity"}, '
123 | '"actor": {"name": "test", "objectType": "Agent"}, "objectType": "SubStatement"}'))
124 |
125 | def agentVerificationHelper(self, value):
126 | self.assertIsInstance(value, Agent)
127 | self.assertEqual(value.name, 'test')
128 |
129 | def groupVerificationHelper(self, value):
130 | self.assertIsInstance(value, Group)
131 | for k in value.member:
132 | self.assertIsInstance(k, Agent)
133 | self.assertEqual(k.name, 'test')
134 |
135 | def verbVerificationHelper(self, value):
136 | self.assertIsInstance(value, Verb)
137 | self.assertEqual(value.id, 'test')
138 |
139 | def statementrefVerificationHelper(self, value):
140 | self.assertIsInstance(value, StatementRef)
141 | self.assertEqual(value.object_type, 'StatementRef')
142 |
143 | def activityVerificationHelper(self, value):
144 | self.assertIsInstance(value, Activity)
145 | self.assertEqual(value.id, 'test')
146 |
147 |
148 | if __name__ == '__main__':
149 | suite = unittest.TestLoader().loadTestsFromTestCase(SubStatementTest)
150 | unittest.TextTestRunner(verbosity=2).run(suite)
151 |
--------------------------------------------------------------------------------
/test/template_test.py.template:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import unittest
16 |
17 | if __name__ == '__main__':
18 | from test.main import setup_tincan_path
19 | setup_tincan_path()
20 | from test.test_utils import TinCanBaseTestCase
21 | from test.resources import lrs_properties
22 |
23 |
24 | class TemplateTest(TinCanBaseTestCase):
25 |
26 | def setUp(self):
27 | """Set up variables before each test."""
28 | #self.my_var = True
29 | #self.connection = lrs_setup_connection()
30 | pass
31 |
32 | def tearDown(self):
33 | """Tear down variables after each test."""
34 | #self.connection.close()
35 | pass
36 |
37 | def test_values(self):
38 | self.assertTrue(True, "Failure message here.")
39 | self.assertFalse(False)
40 | self.assertEquals(1, 1)
41 | self.assertNotEqual(1, 2)
42 |
43 | def test_raises(self):
44 | def raise_exception(*args, **kwargs):
45 | # print "Called raise_exception({args}, {kwargs})" % (args, kwargs)
46 | raise Exception()
47 |
48 | self.assertRaises(Exception, raise_exception, 1, 2, c=3, d=4)
49 |
50 | with self.assertRaises(Exception):
51 | raise_exception(1, 2, c=3, d=4)
52 |
53 | def test_dict(self):
54 | d = {'a': 'My value'}
55 | self.assertIsInstance(d, dict)
56 | self.assertIn('a', d, "d should contain 'a'")
57 | self.assertNotIn('b', d, "d should not contain 'b'")
58 |
59 | def test_lrs_properties(self):
60 | """
61 | Shows how to access user-specific properties from
62 | ``resources.lrs_properties``.
63 | """
64 | self.assertIsNotNone(lrs_properties.username)
65 |
66 |
67 | if __name__ == '__main__':
68 | suite = unittest.TestLoader().loadTestsFromTestCase(TemplateTest)
69 | unittest.TextTestRunner(verbosity=2).run(suite)
--------------------------------------------------------------------------------
/test/test_utils.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import unittest
16 |
17 | if __name__ == '__main__':
18 | from test.main import setup_tincan_path
19 |
20 | setup_tincan_path()
21 | from tincan import Version
22 |
23 |
24 | class TinCanBaseTestCase(unittest.TestCase):
25 | # PEP8 says this should be lowercase, but unittest breaks this rule
26 | def assertSerializeDeserialize(self, obj, version=None):
27 | """
28 | Verifies that objects are packed to JSON and unpacked
29 | correctly for the version specified. If no version is
30 | specified, tests for all versions in TCAPIVersion.
31 | :param obj: A TinCan object to be tested.
32 | :param version: The version according to whose schema we will test.
33 | :type version: str
34 | """
35 | tested_versions = [version] if version is not None else Version.supported
36 | for version in tested_versions:
37 | constructor = obj.__class__.from_json
38 | json_obj = obj.to_json(version)
39 | clone = constructor(json_obj)
40 |
41 | self.assertEqual(obj.__class__, clone.__class__)
42 |
43 | if isinstance(obj, dict):
44 | orig_dict = obj
45 | clone_dict = clone
46 | else:
47 | orig_dict = obj.__dict__
48 | clone_dict = clone.__dict__
49 |
50 | self.assertEqual(orig_dict, clone_dict)
51 |
--------------------------------------------------------------------------------
/test/typedlist_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import unittest
16 |
17 | if __name__ == '__main__':
18 | from test.main import setup_tincan_path
19 |
20 | setup_tincan_path()
21 | from tincan import TypedList
22 |
23 |
24 | class TypedListTest(unittest.TestCase):
25 | def test_Init(self):
26 | with self.assertRaises(ValueError):
27 | TypedList()
28 |
29 |
30 | if __name__ == "__main__":
31 | suite = unittest.TestLoader().loadTestsFromTestCase(TypedListTest)
32 | unittest.TextTestRunner(verbosity=2).run(suite)
33 |
--------------------------------------------------------------------------------
/test/version_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import unittest
16 |
17 | if __name__ == '__main__':
18 | from test.main import setup_tincan_path
19 |
20 | setup_tincan_path()
21 | from tincan import Version
22 |
23 |
24 | class VersionTest(unittest.TestCase):
25 | def test_Supported(self):
26 | self.assertEqual(Version.supported, ["1.0.3", "1.0.2", "1.0.1", "1.0.0"])
27 |
28 | def test_Latest(self):
29 | self.assertEqual(Version.latest, Version.supported[0])
30 |
31 |
32 | if __name__ == '__main__':
33 | suite = unittest.TestLoader().loadTestsFromTestCase(VersionTest)
34 | unittest.TextTestRunner(verbosity=2).run(suite)
35 |
--------------------------------------------------------------------------------
/tincan/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Client library for communicating with an LRS (Learning Record Store)
3 | implementing Tin Can API version 1.0.0 or 1.0.1.
4 |
5 | Web site:
6 |
7 | For more info about the Tin Can API, see .
8 | """
9 |
10 | # These imports are for convenience to external modules only.
11 | # Internal tincan modules should continue to use explicit imports,
12 | # since this file will not have run yet.
13 | #
14 | # For example, from the outside, you can say:
15 | # from tincan import RemoteLRS, LRSResponse
16 | #
17 | # but inside the tincan package, we have to use:
18 | # from tincan.remote_lrs import RemoteLRS
19 | # from tincan.lrs_response import LRSResponse
20 |
21 | from tincan.about import About
22 | from tincan.activity import Activity
23 | from tincan.activity_definition import ActivityDefinition
24 | from tincan.activity_list import ActivityList
25 | from tincan.agent import Agent
26 | from tincan.agent_account import AgentAccount
27 | from tincan.agent_list import AgentList
28 | from tincan.attachment import Attachment
29 | from tincan.attachment_list import AttachmentList
30 | from tincan.base import Base
31 | from tincan.context import Context
32 | from tincan.context_activities import ContextActivities
33 | from tincan.documents.activity_profile_document import ActivityProfileDocument
34 | from tincan.documents.agent_profile_document import AgentProfileDocument
35 | from tincan.documents.document import Document
36 | from tincan.documents.state_document import StateDocument
37 | from tincan.extensions import Extensions
38 | from tincan.group import Group
39 | from tincan.http_request import HTTPRequest
40 | from tincan.interaction_component import InteractionComponent
41 | from tincan.interaction_component_list import InteractionComponentList
42 | from tincan.language_map import LanguageMap
43 | from tincan.lrs_response import LRSResponse
44 | from tincan.remote_lrs import RemoteLRS
45 | from tincan.result import Result
46 | from tincan.score import Score
47 | from tincan.serializable_base import SerializableBase
48 | from tincan.statement import Statement
49 | from tincan.statement_base import StatementBase
50 | from tincan.statement_list import StatementList
51 | from tincan.statement_ref import StatementRef
52 | from tincan.statement_targetable import StatementTargetable
53 | from tincan.statements_result import StatementsResult
54 | from tincan.substatement import SubStatement
55 | from tincan.typed_list import TypedList
56 | from tincan.verb import Verb
57 | from tincan.version import Version
58 |
--------------------------------------------------------------------------------
/tincan/about.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.serializable_base import SerializableBase
16 | from tincan.version import Version
17 | from tincan.extensions import Extensions
18 |
19 |
20 | class About(SerializableBase):
21 | """Stores info about this installation of `tincan`.
22 |
23 | :param version: The versions supported. This attribute is required.
24 | :type version: list of unicode
25 | :param extensions: Custom user data. This attribute is optional.
26 | :type extensions: :class:`tincan.Extensions`
27 | """
28 |
29 | _props_req = [
30 | 'version',
31 | ]
32 | _props = [
33 | 'extensions',
34 | ]
35 | _props.extend(_props_req)
36 |
37 | def __init__(self, *args, **kwargs):
38 | self._version = None
39 | self._extensions = None
40 |
41 | super(About, self).__init__(*args, **kwargs)
42 |
43 | @property
44 | def version(self):
45 | """Version for About
46 |
47 | :setter: Sets the version. If None is provided, defaults to
48 | `[tincan.Version.latest]`. If a string is provided,
49 | makes a 1-element list containing the string.
50 | :setter type: list | tuple | str | unicode | None
51 | :rtype: list
52 | """
53 | return self._version
54 |
55 | @version.setter
56 | def version(self, value):
57 | def check_version(version):
58 | """Checks a single version string for validity. Raises
59 | if invalid.
60 |
61 | :param version: the version string to check
62 | :type version: list of str or tuple of str or basestring or unicode
63 | :raises: ValueError
64 | """
65 | if version in ['1.0.3', '1.0.2', '1.0.1', '1.0.0', '0.95', '0.9']:
66 | return
67 |
68 | # Construct the error message
69 | if isinstance(value, (list, tuple)):
70 | value_str = repr(version) + ' in ' + repr(value)
71 | else:
72 | value_str = repr(version)
73 |
74 | msg = (
75 | f"Tried to set property 'version' in a 'tincan.{self.__class__.__name__}' object "
76 | f"with an invalid value: {value_str}\n"
77 | f"Allowed versions are: {', '.join(map(repr, Version.supported))}"
78 | )
79 |
80 | raise ValueError(msg)
81 |
82 | if value is None:
83 | self._version = [Version.latest]
84 | elif isinstance(value, str):
85 | check_version(value)
86 | self._version = [value]
87 | elif isinstance(value, (list, tuple)):
88 | for v in value:
89 | check_version(v)
90 | self._version = list(value)
91 | else:
92 | raise TypeError(
93 | f"Property 'version' in a 'tincan.{self.__class__.__name__}' object must be set with a "
94 | f"list, tuple, str, unicode or None. Tried to set it with: {repr(value)}"
95 | )
96 |
97 | @property
98 | def extensions(self):
99 | """Extensions for About
100 |
101 | :setter: Tries to convert to :class:`tincan.Extensions`. If None is provided,
102 | sets to an empty :class:`tincan.Extensions` dict.
103 | :setter type: :class:`tincan.Extensions` | dict | None
104 | :rtype: :class:`tincan.Extensions`
105 | """
106 | return self._extensions
107 |
108 | @extensions.setter
109 | def extensions(self, value):
110 | if isinstance(value, Extensions):
111 | self._extensions = value
112 | elif value is None:
113 | self._extensions = Extensions()
114 | else:
115 | try:
116 | self._extensions = Extensions(value)
117 | except Exception as e:
118 | msg = (
119 | f"Property 'extensions' in a 'tincan.{self.__class__.__name__} object must be set with a "
120 | f"tincan.Extensions, dict, or None.\n\n"
121 | )
122 | msg += repr(e)
123 | raise TypeError(msg)
124 |
125 | @extensions.deleter
126 | def extensions(self):
127 | del self._extensions
128 |
--------------------------------------------------------------------------------
/tincan/activity.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.serializable_base import SerializableBase
16 | from tincan.statement_targetable import StatementTargetable
17 | from tincan.activity_definition import ActivityDefinition
18 |
19 | """
20 | .. module:: activity
21 | :synopsis: The Activity object that defines the boundaries on the 'Object' \
22 | part of 'Actor-Verb-Object' structure of a Statement
23 |
24 | """
25 |
26 |
27 | class Activity(SerializableBase, StatementTargetable):
28 | _props_req = [
29 | 'id',
30 | 'object_type'
31 | ]
32 |
33 | _props = [
34 | 'definition',
35 | ]
36 |
37 | _props.extend(_props_req)
38 |
39 | def __init__(self, *args, **kwargs):
40 | self._id = None
41 | self._object_type = None
42 | self._definition = None
43 |
44 | super(Activity, self).__init__(*args, **kwargs)
45 |
46 | @property
47 | def id(self):
48 | """Id for Activity
49 |
50 | :setter: Sets the id
51 | :setter type: unicode
52 | :rtype: unicode
53 |
54 | """
55 | return self._id
56 |
57 | @id.setter
58 | def id(self, value):
59 | if value is not None:
60 | if value == '':
61 | raise ValueError(
62 | f"Property 'id' in 'tincan.{self.__class__.__name__}' object must be not empty."
63 | )
64 | self._id = None if value is None else str(value)
65 |
66 | @property
67 | def object_type(self):
68 | """Object type for Activity. Will always be "Activity"
69 |
70 | :setter: Tries to convert to unicode
71 | :setter type: unicode
72 | :rtype: unicode
73 |
74 | """
75 | return self._object_type
76 |
77 | @object_type.setter
78 | def object_type(self, _):
79 | self._object_type = 'Activity'
80 |
81 | @property
82 | def definition(self):
83 | """Definition for Activity
84 |
85 | :setter: Tries to convert to :class:`tincan.ActivityDefinition`
86 | :setter type: :class:`tincan.ActivityDefinition`
87 | :rtype: :class:`tincan.ActivityDefinition`
88 |
89 | """
90 | return self._definition
91 |
92 | @definition.setter
93 | def definition(self, value):
94 | if value is not None and not isinstance(value, ActivityDefinition):
95 | value = ActivityDefinition(value)
96 | self._definition = value
97 |
98 | @definition.deleter
99 | def definition(self):
100 | del self._definition
101 |
--------------------------------------------------------------------------------
/tincan/activity_list.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.activity import Activity
16 | from tincan.typed_list import TypedList
17 |
18 | """
19 | .. module:: activity_list
20 | :synopsis: A wrapper for a list that is able to type check
21 |
22 | """
23 |
24 |
25 | class ActivityList(TypedList):
26 | _cls = Activity
27 |
--------------------------------------------------------------------------------
/tincan/agent.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | from tincan.serializable_base import SerializableBase
15 | from tincan.agent_account import AgentAccount
16 |
17 | """
18 |
19 | .. module:: Agent
20 | :synopsis: The Agent object that contains the information about an Agent
21 |
22 | """
23 |
24 |
25 | class Agent(SerializableBase):
26 | _props_req = [
27 | "object_type"
28 | ]
29 |
30 | _props = [
31 | "name",
32 | "mbox",
33 | "mbox_sha1sum",
34 | "openid",
35 | "account"
36 | ]
37 |
38 | _props.extend(_props_req)
39 |
40 | def __init__(self, *args, **kwargs):
41 | self._object_type = None
42 | self._name = None
43 | self._mbox = None
44 | self._mbox_sha1sum = None
45 | self._openid = None
46 | self._account = None
47 |
48 | super(Agent, self).__init__(*args, **kwargs)
49 |
50 | @property
51 | def object_type(self):
52 | """Object Type for Agent. Will always be 'Agent'
53 |
54 | :setter: Tries to convert to unicode
55 | :setter type: unicode
56 | :rtype: unicode
57 |
58 | """
59 | return self._object_type
60 |
61 | @object_type.setter
62 | def object_type(self, _):
63 | self._object_type = 'Agent'
64 |
65 | @property
66 | def name(self):
67 | """Name for Agent
68 |
69 | :setter: Tries to convert to unicode
70 | :setter type: unicode
71 | :rtype: unicode
72 |
73 | """
74 | return self._name
75 |
76 | @name.setter
77 | def name(self, value):
78 | if value is not None:
79 | if value == '':
80 | raise ValueError("Property name can not be set to an empty string")
81 | elif not isinstance(value, str):
82 | value = str(value)
83 | self._name = value
84 |
85 | @name.deleter
86 | def name(self):
87 | del self._name
88 |
89 | @property
90 | def mbox(self):
91 | """Mbox for Agent
92 |
93 | :setter: Tries to convert to unicode
94 | :setter type: unicode
95 | :rtype: unicode
96 |
97 | """
98 | return self._mbox
99 |
100 | @mbox.setter
101 | def mbox(self, value):
102 | if value is not None:
103 | if value == '':
104 | raise ValueError("Property mbox can not be set to an empty string")
105 | elif not isinstance(value, str):
106 | value = str(value)
107 | if not value.startswith("mailto:"):
108 | value = "mailto:" + value
109 | self._mbox = value
110 |
111 | @mbox.deleter
112 | def mbox(self):
113 | del self._mbox
114 |
115 | @property
116 | def mbox_sha1sum(self):
117 | """Mbox_sha1sum for Agent
118 |
119 | :setter: Tries to convert to unicode
120 | :setter type: unicode
121 | :rtype: unicode
122 |
123 | """
124 | return self._mbox_sha1sum
125 |
126 | @mbox_sha1sum.setter
127 | def mbox_sha1sum(self, value):
128 | if value is not None:
129 | if value == '':
130 | raise ValueError("Property mbox_sha1sum can not be set to an empty string")
131 | elif not isinstance(value, str):
132 | value = str(value)
133 | self._mbox_sha1sum = value
134 |
135 | @mbox_sha1sum.deleter
136 | def mbox_sha1sum(self):
137 | del self._mbox_sha1sum
138 |
139 | @property
140 | def openid(self):
141 | """Openid for Agent
142 |
143 | :setter: Tries to convert to unicode
144 | :setter type: unicode
145 | :rtype: unicode
146 |
147 | """
148 | return self._openid
149 |
150 | @openid.setter
151 | def openid(self, value):
152 | if value is not None:
153 | if value == '':
154 | raise ValueError("Property openid can not be set to an empty string")
155 | elif not isinstance(value, str):
156 | value = str(value)
157 | self._openid = value
158 |
159 | @openid.deleter
160 | def openid(self):
161 | del self._openid
162 |
163 | @property
164 | def account(self):
165 | """Account for Agent
166 |
167 | :setter: Tries to convert to :class:`tincan.AgentAccount`
168 | :setter type: :class:`tincan.AgentAccount`
169 | :rtype: :class:`tincan.AgentAccount`
170 |
171 | """
172 | return self._account
173 |
174 | @account.setter
175 | def account(self, value):
176 | if value is not None and not isinstance(value, AgentAccount):
177 | value = AgentAccount(value)
178 | self._account = value
179 |
180 | @account.deleter
181 | def account(self):
182 | del self._account
183 |
--------------------------------------------------------------------------------
/tincan/agent_account.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.serializable_base import SerializableBase
16 |
17 | """
18 |
19 | .. module:: Agent Account
20 | :synopsis: The account object that can be included in an agent
21 |
22 | """
23 |
24 |
25 | class AgentAccount(SerializableBase):
26 | _props = [
27 | "name",
28 | "home_page"
29 | ]
30 |
31 | def __init__(self, *args, **kwargs):
32 | self._name = None
33 | self._home_page = None
34 |
35 | super(AgentAccount, self).__init__(*args, **kwargs)
36 |
37 | @property
38 | def name(self):
39 | """Name for Account
40 |
41 | :setter: Tries to convert to unicode
42 | :setter type: unicode
43 | :rtype: unicode
44 |
45 | """
46 | return self._name
47 |
48 | @name.setter
49 | def name(self, value):
50 | if value is not None:
51 | if value == '':
52 | raise ValueError("Property name can not be set to an empty string")
53 | elif not isinstance(value, str):
54 | value = str(value)
55 | self._name = value
56 |
57 | @name.deleter
58 | def name(self):
59 | del self._name
60 |
61 | @property
62 | def home_page(self):
63 | """Homepage for Account
64 |
65 | :setter: Tries to convert to unicode
66 | :setter type: unicode
67 | :rtype: unicode
68 |
69 | """
70 | return self._home_page
71 |
72 | @home_page.setter
73 | def home_page(self, value):
74 | if value is not None:
75 | if value == '':
76 | raise ValueError("Property homepage can not be set to an empty string")
77 | elif not isinstance(value, str):
78 | value = str(value)
79 | self._home_page = value
80 |
81 | @home_page.deleter
82 | def home_page(self):
83 | del self._home_page
84 |
--------------------------------------------------------------------------------
/tincan/agent_list.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.agent import Agent
16 | from tincan.typed_list import TypedList
17 |
18 | """
19 | .. module:: agent_list
20 | :synopsis: A wrapper for list that is able to type check
21 |
22 | """
23 |
24 |
25 | class AgentList(TypedList):
26 | _cls = Agent
27 |
--------------------------------------------------------------------------------
/tincan/attachment.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.serializable_base import SerializableBase
16 | from tincan.language_map import LanguageMap
17 |
18 | """
19 | .. module:: Attachment
20 | :synopsis: An Attachment object that contains an attached file and its information
21 |
22 | """
23 |
24 |
25 | class Attachment(SerializableBase):
26 | _props_req = [
27 | "usage_type",
28 | "display",
29 | "content_type",
30 | "length",
31 | "sha2"
32 | ]
33 |
34 | _props = [
35 | "description",
36 | "fileurl"
37 | ]
38 |
39 | _props.extend(_props_req)
40 |
41 | def __init__(self, *args, **kwargs):
42 | self._usage_type = None
43 | self._display = None
44 | self._content_type = None
45 | self._length = None
46 | self._sha2 = None
47 | self._description = None
48 | self._fileurl = None
49 |
50 | super(Attachment, self).__init__(*args, **kwargs)
51 |
52 | @property
53 | def usage_type(self):
54 | """Usage type for Attachment
55 |
56 | :setter: Tries to convert to unicode
57 | :setter type: unicode
58 | :rtype: unicode
59 |
60 | """
61 | return self._usage_type
62 |
63 | @usage_type.setter
64 | def usage_type(self, value):
65 | if value is not None:
66 | if value == '':
67 | raise ValueError("Property usage_type can not be set to an empty string")
68 | elif not isinstance(value, str):
69 | value = str(value)
70 | self._usage_type = value
71 |
72 | @usage_type.deleter
73 | def usage_type(self):
74 | del self._usage_type
75 |
76 | @property
77 | def content_type(self):
78 | """Content type for Attachment
79 |
80 | :setter: Tries to convert to unicode
81 | :setter type: unicode
82 | :rtype: unicode
83 |
84 | """
85 | return self._content_type
86 |
87 | @content_type.setter
88 | def content_type(self, value):
89 | if value is not None:
90 | if value == '':
91 | raise ValueError("Property content_type can not be set to an empty string")
92 | elif not isinstance(value, str):
93 | value = str(value)
94 | self._content_type = value
95 |
96 | @content_type.deleter
97 | def content_type(self):
98 | del self._content_type
99 |
100 | @property
101 | def length(self):
102 | """Usage type for Attachment
103 |
104 | :setter: Tries to convert to long
105 | :setter type: int | long
106 | :rtype: long
107 |
108 | """
109 | return self._length
110 |
111 | @length.setter
112 | def length(self, value):
113 | if value is not None:
114 | if not isinstance(value, int):
115 | value = int(value)
116 | self._length = value
117 |
118 | @length.deleter
119 | def length(self):
120 | del self._length
121 |
122 | @property
123 | def sha2(self):
124 | """Sha2 for Attachment
125 |
126 | :setter: Tries to convert to unicode
127 | :setter type: unicode
128 | :rtype: unicode
129 |
130 | """
131 | return self._sha2
132 |
133 | @sha2.setter
134 | def sha2(self, value):
135 | if value is not None:
136 | if value == '':
137 | raise ValueError("Property sha2 can not be set to an empty string")
138 | elif not isinstance(value, str):
139 | value = str(value)
140 | self._sha2 = value
141 |
142 | @sha2.deleter
143 | def sha2(self):
144 | del self._sha2
145 |
146 | @property
147 | def fileurl(self):
148 | """File URL for Attachment
149 |
150 | :setter: Tries to convert to unicode
151 | :setter type: unicode
152 | :rtype: unicode
153 |
154 | """
155 | return self._fileurl
156 |
157 | @fileurl.setter
158 | def fileurl(self, value):
159 | if value is not None:
160 | if value == '':
161 | raise ValueError("Property fileurl can not be set to an empty string")
162 | elif not isinstance(value, str):
163 | value = str(value)
164 | self._fileurl = value
165 |
166 | @fileurl.deleter
167 | def fileurl(self):
168 | del self._fileurl
169 |
170 | @property
171 | def display(self):
172 | """Display for Attachment
173 |
174 | :setter: Tries to convert to :class:`tincan.LanguageMap`
175 | :setter type: :class:`tincan.LanguageMap`
176 | :rtype: :class:`tincan.LanguageMap`
177 |
178 | """
179 | return self._display
180 |
181 | @display.setter
182 | def display(self, value):
183 | if value is not None and not isinstance(value, LanguageMap):
184 | value = LanguageMap(value)
185 | self._display = value
186 |
187 | @display.deleter
188 | def display(self):
189 | del self._display
190 |
191 | @property
192 | def description(self):
193 | """Description for Attachment
194 |
195 | :setter: Tries to convert to :class:`tincan.LanguageMap`
196 | :setter type: :class:`tincan.LanguageMap`
197 | :rtype: :class:`tincan.LanguageMap`
198 |
199 | """
200 | return self._description
201 |
202 | @description.setter
203 | def description(self, value):
204 | if value is not None and not isinstance(value, LanguageMap):
205 | value = LanguageMap(value)
206 | self._description = value
207 |
208 | @description.deleter
209 | def description(self):
210 | del self._description
211 |
--------------------------------------------------------------------------------
/tincan/attachment_list.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.attachment import Attachment
16 | from tincan.typed_list import TypedList
17 |
18 | """
19 | .. module:: attachment_list
20 | :synopsis: A wrapper for a list that is able to type check
21 |
22 | """
23 |
24 |
25 | class AttachmentList(TypedList):
26 | _cls = Attachment
27 |
--------------------------------------------------------------------------------
/tincan/base.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """
16 | .. module:: base
17 | :synopsis: A base object to provide a common initializer for objects to
18 | easily keep track of required and optional properties and their
19 | error checking
20 |
21 | """
22 |
23 |
24 | class Base(object):
25 | _props = []
26 |
27 | def __init__(self, *args, **kwargs):
28 | """Initializes an object by checking the provided arguments
29 | against lists defined in the individual class. If required
30 | properties are defined, this method will set them to None by default.
31 | Optional properties will be ignored if they are not provided. The
32 | class may provide custom setters for properties, in which case those
33 | setters (see __setattr__ below).
34 |
35 | """
36 | if hasattr(self, '_props_req') and self._props_req:
37 | list(map(lambda k: setattr(self, k, None), self._props_req))
38 |
39 | new_kwargs = {}
40 | for obj in args:
41 | new_kwargs.update(obj if isinstance(obj, dict) else vars(obj))
42 |
43 | new_kwargs.update(kwargs)
44 |
45 | for key, value in new_kwargs.items():
46 | setattr(self, key, value)
47 |
48 | def __setattr__(self, attr, value):
49 | """Makes sure that only allowed properties are set. This method will
50 | call the proper attribute setter as defined in the class to provide
51 | additional error checking
52 |
53 | :param attr: the attribute being set
54 | :type attr: str
55 | :param value: the value to set
56 |
57 | """
58 | if attr.startswith('_') and attr[1:] in self._props:
59 | super(Base, self).__setattr__(attr, value)
60 | elif attr not in self._props:
61 | raise AttributeError(
62 | f"Property '{attr}' cannot be set on a 'tincan.{self.__class__.__name__}' object."
63 | f"Allowed properties: {', '.join(self._props)}"
64 | )
65 | else:
66 | super(Base, self).__setattr__(attr, value)
67 |
68 | def __eq__(self, other):
69 | return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
70 |
--------------------------------------------------------------------------------
/tincan/context_activities.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.serializable_base import SerializableBase
16 | from tincan.activity_list import ActivityList
17 | from tincan.activity import Activity
18 |
19 |
20 | class ContextActivities(SerializableBase):
21 | _props = [
22 | 'category',
23 | 'parent',
24 | 'grouping',
25 | 'other',
26 | ]
27 |
28 | def __init__(self, *args, **kwargs):
29 | self._category = None
30 | self._parent = None
31 | self._grouping = None
32 | self._other = None
33 |
34 | super(ContextActivities, self).__init__(*args, **kwargs)
35 |
36 | @property
37 | def category(self):
38 | """Category for Context Activities
39 |
40 | :setter: Tries to convert to :class:`tincan.ActivityList`
41 | :setter type: :class:`tincan.ActivityList`
42 | :rtype: :class:`tincan.ActivityList`
43 |
44 | """
45 | return self._category
46 |
47 | @category.setter
48 | def category(self, value):
49 | value = self._activity_or_list(value)
50 | self._category = value
51 |
52 | @category.deleter
53 | def category(self):
54 | del self._category
55 |
56 | @property
57 | def parent(self):
58 | """Parent for Context Activities
59 |
60 | :setter: Tries to convert to :class:`tincan.ActivityList`
61 | :setter type: :class:`tincan.ActivityList`
62 | :rtype: :class:`tincan.ActivityList`
63 |
64 | """
65 | return self._parent
66 |
67 | @parent.setter
68 | def parent(self, value):
69 | value = self._activity_or_list(value)
70 | self._parent = value
71 |
72 | @parent.deleter
73 | def parent(self):
74 | del self._parent
75 |
76 | @property
77 | def grouping(self):
78 | """Grouping for Context Activities
79 |
80 | :setter: Tries to convert to :class:`tincan.ActivityList`
81 | :setter type: :class:`tincan.ActivityList`
82 | :rtype: :class:`tincan.ActivityList`
83 |
84 | """
85 | return self._grouping
86 |
87 | @grouping.setter
88 | def grouping(self, value):
89 | value = self._activity_or_list(value)
90 | self._grouping = value
91 |
92 | @grouping.deleter
93 | def grouping(self):
94 | del self._grouping
95 |
96 | @property
97 | def other(self):
98 | """Other for Context Activities
99 |
100 | :setter: Tries to convert to :class:`tincan.ActivityList`
101 | :setter type: :class:`tincan.ActivityList`
102 | :rtype: :class:`tincan.ActivityList`
103 |
104 | """
105 | return self._other
106 |
107 | @other.setter
108 | def other(self, value):
109 | value = self._activity_or_list(value)
110 | self._other = value
111 |
112 | @other.deleter
113 | def other(self):
114 | del self._other
115 |
116 | @staticmethod
117 | def _activity_or_list(value):
118 | """Tries to convert value to :class:`tincan.ActivityList`
119 |
120 | :setter type: :class:`tincan.ActivityList`
121 | :rtype: :class:`tincan.ActivityList`
122 | """
123 | result = value
124 | if value is not None and not isinstance(value, ActivityList):
125 | try:
126 | result = ActivityList([Activity(value)])
127 | except (TypeError, AttributeError):
128 | result = ActivityList(value)
129 | return result
130 |
--------------------------------------------------------------------------------
/tincan/conversions/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RusticiSoftware/TinCanPython/bbc3f9dd5d8385e7b66c693e7f8262561392be74/tincan/conversions/__init__.py
--------------------------------------------------------------------------------
/tincan/documents/__init__.py:
--------------------------------------------------------------------------------
1 | from tincan.documents.document import Document
2 | from tincan.documents.state_document import StateDocument
3 | from tincan.documents.activity_profile_document import ActivityProfileDocument
4 | from tincan.documents.agent_profile_document import AgentProfileDocument
5 |
--------------------------------------------------------------------------------
/tincan/documents/activity_profile_document.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | from tincan.documents import Document
15 | from tincan.activity import Activity
16 |
17 |
18 | class ActivityProfileDocument(Document):
19 | """Extends :class:`tincan.Document` with an Activity field, can be created from a dict, another
20 | :class:`tincan.Document`, or from kwargs.
21 |
22 | :param id: The id of this document
23 | :type id: unicode
24 | :param content_type: The content_type of the content of this document
25 | :type content_type: unicode
26 | :param content: The content of this document
27 | :type content: bytearray
28 | :param etag: The etag of this document
29 | :type etag: unicode
30 | :param timestamp: The timestamp of this document
31 | :type timestamp: :class:`datetime.datetime`
32 | :param activity: The activity object of this document
33 | :type activity: :class:`tincan.Activity`
34 | """
35 |
36 | _props_req = list(Document._props_req)
37 |
38 | _props_req.extend([
39 | 'activity',
40 | ])
41 |
42 | _props = list(Document._props)
43 |
44 | _props.extend(_props_req)
45 |
46 | def __init__(self, *args, **kwargs):
47 | self._activity = None
48 | super(ActivityProfileDocument, self).__init__(*args, **kwargs)
49 |
50 | @property
51 | def activity(self):
52 | """The Document's activity object
53 |
54 | :setter: Tries to convert to :class:`tincan.Activity`
55 | :setter type: :class:`tincan.Activity`
56 | :rtype: :class:`tincan.Activity`
57 | """
58 | return self._activity
59 |
60 | @activity.setter
61 | def activity(self, value):
62 | if not isinstance(value, Activity) and value is not None:
63 | try:
64 | value = Activity(value)
65 | except:
66 | raise TypeError(
67 | f"Property 'activity' in 'tincan.{self.__class__.__name__}' must be set with a type "
68 | f"that can be constructed into an tincan.Activity object."
69 | )
70 | self._activity = value
71 |
--------------------------------------------------------------------------------
/tincan/documents/agent_profile_document.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | from tincan.documents import Document
15 | from tincan.agent import Agent
16 |
17 |
18 | class AgentProfileDocument(Document):
19 | """Extends :class:`tincan.Document` with an Agent field, can be created from a dict,
20 | another :class:`tincan.Document`, or from kwargs.
21 |
22 | :param id: The id of this document
23 | :type id: unicode
24 | :param content_type: The content_type of the content of this document
25 | :type content_type: unicode
26 | :param content: The content of this document
27 | :type content: bytearray
28 | :param etag: The etag of this document
29 | :type etag: unicode
30 | :param timestamp: The timestamp of this document
31 | :type timestamp: :class:`datetime.datetime`
32 | :param agent: The agent object of this document
33 | :type agent: :class:`tincan.Agent`
34 | """
35 |
36 | _props_req = list(Document._props_req)
37 |
38 | _props_req.extend([
39 | 'agent',
40 | ])
41 |
42 | _props = list(Document._props)
43 |
44 | _props.extend(_props_req)
45 |
46 | def __init__(self, *args, **kwargs):
47 | self._agent = None
48 | super(AgentProfileDocument, self).__init__(*args, **kwargs)
49 |
50 | @property
51 | def agent(self):
52 | """The Document's agent object
53 |
54 | :setter: Tries to convert to :class:`tincan.Agent`
55 | :setter type: :class:`tincan.Agent`
56 | :rtype: :class:`tincan.Agent`
57 | """
58 | return self._agent
59 |
60 | @agent.setter
61 | def agent(self, value):
62 | if not isinstance(value, Agent) and value is not None:
63 | try:
64 | value = Agent(value)
65 | except:
66 | raise TypeError(
67 | f"Property 'agent' in 'tincan.{self.__class__.__name__}' must be set with a type "
68 | f"that can be constructed into an tincan.Agent object."
69 | )
70 | self._agent = value
71 |
--------------------------------------------------------------------------------
/tincan/documents/document.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | import datetime
15 |
16 | from tincan.base import Base
17 | from tincan.conversions.iso8601 import make_datetime
18 |
19 |
20 | class Document(Base):
21 | """Document class can be instantiated from a dict, another :class:`tincan.Document`, or from kwargs
22 |
23 | :param id: The id of this document
24 | :type id: unicode
25 | :param content_type: The content type of the content of this document
26 | :type content_type: unicode
27 | :param content: The content of this document
28 | :type content: bytearray
29 | :param etag: The etag of this document
30 | :type etag: unicode
31 | :param timestamp: The timestamp of this document
32 | :type timestamp: :class:`datetime.datetime`
33 |
34 | """
35 | _props_req = [
36 | 'id',
37 | 'content',
38 | 'content_type',
39 | 'etag',
40 | 'timestamp',
41 | ]
42 |
43 | _props = []
44 |
45 | _props.extend(_props_req)
46 |
47 | def __init__(self, *args, **kwargs):
48 | self._id = None
49 | self._content = None
50 | self._content_type = None
51 | self._etag = None
52 | self._timestamp = None
53 |
54 | super(Document, self).__init__(*args, **kwargs)
55 |
56 | @property
57 | def id(self):
58 | """The Document id
59 |
60 | :setter: Tries to convert to unicode
61 | :setter type: str | unicode
62 | :rtype: unicode
63 |
64 | """
65 | return self._id
66 |
67 | @id.setter
68 | def id(self, value):
69 | if isinstance(value, (bytes, bytearray)) and value is not None:
70 | value = value.decode("utf-8")
71 | self._id = value
72 |
73 | @property
74 | def content_type(self):
75 | """The Document content type
76 |
77 | :setter: Tries to convert to unicode
78 | :setter type: str | unicode
79 | :rtype: unicode
80 |
81 | """
82 | return self._content_type
83 |
84 | @content_type.setter
85 | def content_type(self, value):
86 | if isinstance(value, (bytes, bytearray)) and value is not None:
87 | value = value.decode("utf-8")
88 | self._content_type = value
89 |
90 | @property
91 | def content(self):
92 | """The Document content
93 |
94 | :setter: Tries to convert to bytearray.
95 | :setter type: str | unicode | bytearray
96 | :rtype: bytearray
97 |
98 | """
99 | return self._content
100 |
101 | @content.setter
102 | def content(self, value):
103 | if not isinstance(value, bytearray) and value is not None:
104 | value = bytearray(value, "utf-8")
105 |
106 | self._content = value
107 |
108 | @property
109 | def etag(self):
110 | """The Document etag
111 |
112 | :setter: Tries to convert to unicode
113 | :setter type: str | unicode
114 | :rtype: unicode
115 |
116 | """
117 | return self._etag
118 |
119 | @etag.setter
120 | def etag(self, value):
121 | if isinstance(value, (bytes, bytearray)) and value is not None:
122 | value = value.decode("utf-8")
123 | self._etag = value
124 |
125 | @property
126 | def timestamp(self):
127 | """The Document timestamp.
128 |
129 | :setter: Tries to convert to :class:`datetime.datetime`. If
130 | no timezone is given, makes a naive `datetime.datetime`.
131 |
132 | Strings will be parsed as ISO 8601 timestamps.
133 |
134 | If a number is provided, it will be interpreted as a UNIX
135 | timestamp, which by definition is UTC.
136 |
137 | If a `dict` is provided, does `datetime.datetime(**value)`.
138 |
139 | If a `tuple` or a `list` is provided, does
140 | `datetime.datetime(*value)`. Uses the timezone in the tuple or
141 | list if provided.
142 |
143 | :setter type: :class:`datetime.datetime` | unicode | str | int | float | dict | tuple | None
144 | :rtype: :class:`datetime.datetime`
145 |
146 | """
147 | return self._timestamp
148 |
149 | @timestamp.setter
150 | def timestamp(self, value):
151 | if value is None:
152 | self._timestamp = value
153 | return
154 |
155 | try:
156 | self._timestamp = make_datetime(value)
157 | except TypeError as e:
158 | message = (
159 | f"Property 'timestamp' in a 'tincan.{self.__class__.__name__}' "
160 | f"object must be set with a "
161 | f"datetime.datetime, str, unicode, int, float, dict "
162 | f"or None.\n\n{repr(e)}"
163 | )
164 | raise TypeError(message) from e
165 |
--------------------------------------------------------------------------------
/tincan/documents/state_document.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.documents import Document
16 | from tincan.agent import Agent
17 | from tincan.activity import Activity
18 |
19 |
20 | class StateDocument(Document):
21 | """Extends :class:`tincan.Document` with Agent, Activity, and Registration fields; can be created from a dict,
22 | another :class:`tincan.Document`, or from kwargs.
23 |
24 | :param id: The id of this document
25 | :type id: unicode
26 | :param content_type: The content_type of the content of this document
27 | :type content_type: unicode
28 | :param content: The content of this document
29 | :type content: bytearray
30 | :param etag: The etag of this document
31 | :type etag: unicode
32 | :param timestamp: The time stamp of this document
33 | :type timestamp: :class:`datetime.datetime`
34 | :param agent: The agent object of this document
35 | :type agent: :class:`tincan.Agent`
36 | :param activity: The activity object of this document
37 | :type activity: :class:`Activity`
38 | :param registration: The registration id of the state for this document
39 | :type registration: unicode
40 | """
41 |
42 | _props_req = list(Document._props_req)
43 |
44 | _props_req.extend([
45 | 'agent',
46 | 'activity',
47 | 'registration',
48 | ])
49 |
50 | _props = list(Document._props)
51 |
52 | _props.extend(_props_req)
53 |
54 | def __init__(self, *args, **kwargs):
55 | self._agent = None
56 | self._activity = None
57 | self._registration = None
58 |
59 | super(StateDocument, self).__init__(*args, **kwargs)
60 |
61 | @property
62 | def agent(self):
63 | """The Document's agent object
64 |
65 | :setter: Tries to convert to :class:`tincan.Agent`
66 | :setter type: :class:`tincan.Agent`
67 | :rtype: :class:`tincan.Agent`
68 | """
69 | return self._agent
70 |
71 | @agent.setter
72 | def agent(self, value):
73 | if not isinstance(value, Agent) and value is not None:
74 | try:
75 | value = Agent(value)
76 | except:
77 | raise TypeError(
78 | f"Property 'agent' in 'tincan.{self.__class__.__name__}' must be set with a type "
79 | f"that can be constructed into a tincan.Agent object."
80 | )
81 | self._agent = value
82 |
83 | @property
84 | def activity(self):
85 | """The Document's activity object
86 |
87 | :setter: Tries to convert to activity
88 | :setter type: :class:`tincan.Activity`
89 | :rtype: :class:`tincan.Activity`
90 | """
91 | return self._activity
92 |
93 | @activity.setter
94 | def activity(self, value):
95 | if not isinstance(value, Activity) and value is not None:
96 | try:
97 | value = Activity(value)
98 | except:
99 | raise TypeError(
100 | f"Property 'activity' in 'tincan.{self.__class__.__name__}' must be set with a type "
101 | f"that can be constructed into a tincan.Activity object."
102 | )
103 | self._activity = value
104 |
105 | @property
106 | def registration(self):
107 | """The Document registration id
108 |
109 | :setter: Tries to convert to unicode
110 | :setter type: str | unicode | :class:`uuid.UUID`
111 | :rtype: unicode
112 | """
113 | return self._registration
114 |
115 | @registration.setter
116 | def registration(self, value):
117 | if not isinstance(value, str) and value is not None:
118 | str(value)
119 |
120 | self._registration = value
121 |
--------------------------------------------------------------------------------
/tincan/extensions.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.serializable_base import SerializableBase
16 |
17 |
18 | class Extensions(dict, SerializableBase):
19 | """
20 | Contains domain-specific custom data.
21 |
22 | Can be created from a dict, another :class:`tincan.Extensions`,
23 | or from args and kwargs.
24 |
25 | Use this like a regular Python dict.
26 | """
27 |
28 | def __init__(self, *args, **kwargs):
29 | super(Extensions, self).__init__(*args, **kwargs)
30 |
--------------------------------------------------------------------------------
/tincan/group.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.agent import Agent
16 | from tincan.agent_list import AgentList
17 |
18 | """
19 |
20 | .. module:: Group
21 | :synopsis: An object that contains a group of Agents
22 |
23 | """
24 |
25 |
26 | class Group(Agent):
27 | _props = [
28 | "member"
29 | ]
30 |
31 | _props.extend(Agent._props)
32 |
33 | def __init__(self, *args, **kwargs):
34 | self._object_type = None
35 | self._member = AgentList()
36 |
37 | super(Group, self).__init__(*args, **kwargs)
38 |
39 | def addmember(self, value):
40 | """Adds a single member to this group's list of members.
41 | Tries to convert to :class:`tincan.Agent`
42 |
43 | :param value: The member to add to this group
44 | :type value: :class:`tincan.Agent`
45 |
46 | """
47 |
48 | if value is not None and not isinstance(value, Agent):
49 | value = Agent(value)
50 |
51 | self._member.append(value)
52 |
53 | @property
54 | def member(self):
55 | """Members for Group
56 |
57 | :setter: Tries to convert to :class:`tincan.AgentList`
58 | :setter type: :class:`tincan.AgentList`
59 | :rtype: :class:`tincan.AgentList`
60 | """
61 | return self._member
62 |
63 | @member.setter
64 | def member(self, value):
65 | newmembers = AgentList()
66 | if value is not None:
67 | if isinstance(value, list):
68 | for k in value:
69 | if not isinstance(k, Agent):
70 | newmembers.append(Agent(k))
71 | else:
72 | newmembers.append(k)
73 | else:
74 | newmembers = AgentList(value)
75 | self._member = newmembers
76 |
77 | @member.deleter
78 | def member(self):
79 | del self._member
80 |
81 | @property
82 | def object_type(self):
83 | """Object type for Group. Will always be "Group"
84 |
85 | :setter: Tries to convert to unicode
86 | :setter type: unicode
87 | :rtype: unicode
88 |
89 | """
90 | return self._object_type
91 |
92 | @object_type.setter
93 | def object_type(self, _):
94 | self._object_type = 'Group'
95 |
--------------------------------------------------------------------------------
/tincan/http_request.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | from tincan.base import Base
15 |
16 |
17 | class HTTPRequest(Base):
18 | """Creates a new HTTPRequest object, either from a dict, another object, or from kwargs
19 |
20 | :param method: Method for the HTTP connection ("GET", "POST", "DELETE", etc.)
21 | :type method: unicode
22 | :param resource: Resource for the LRS HTTP connection ("about", "statements", "activities/state", etc.)
23 | :type resource: unicode
24 | :param headers: Headers for the HTTP connection ("If-Match", "Content-Type", etc.)
25 | :type headers: dict(unicode:unicode)
26 | :param query_params: Query parameters for the HTTP connection ("registration", "since", "statementId", etc.)
27 | :type query_params: dict(unicode:unicode)
28 | :param content: Content body for the HTTP connection. Valid json string.
29 | :type content: unicode
30 | :param ignore404: True if this request should consider a 404 response successful, False otherwise
31 | :type ignore404: bool
32 | """
33 |
34 | _props_req = [
35 | 'method',
36 | 'resource',
37 | 'headers',
38 | 'query_params',
39 | ]
40 |
41 | _props = [
42 | 'content',
43 | 'ignore404',
44 | ]
45 |
46 | _props.extend(_props_req)
47 |
48 | def __init__(self, *args, **kwargs):
49 | self._method = None
50 | self._resource = None
51 | self._headers = None
52 | self._query_params = None
53 | self._content = None
54 | self._ignore404 = None
55 |
56 | super(HTTPRequest, self).__init__(*args, **kwargs)
57 |
58 | @property
59 | def method(self):
60 | """Method for the HTTP connection ("GET", "POST", "DELETE", etc.)
61 |
62 | :setter: Tries to convert to unicode
63 | :setter type: str | unicode
64 | :rtype: unicode
65 | """
66 | return self._method
67 |
68 | @method.setter
69 | def method(self, value):
70 | if not isinstance(value, str) and value is not None:
71 | str(value)
72 | self._method = value
73 |
74 | @property
75 | def resource(self):
76 | """Resource for the LRS HTTP connection ("about", "statements", "activities/state", etc.)
77 |
78 | :setter: Tries to convert to unicode
79 | :setter type: str | unicode
80 | :rtype: unicode
81 | """
82 | return self._resource
83 |
84 | @resource.setter
85 | def resource(self, value):
86 | if not isinstance(value, str) and value is not None:
87 | str(value)
88 | self._resource = value
89 |
90 | @property
91 | def headers(self):
92 | """Headers for the HTTP connection ("If-Match", "Content-Type", etc.)
93 |
94 | :setter: Accepts a dict or an object
95 | :setter type: dict
96 | :rtype: dict
97 | """
98 | return self._headers
99 |
100 | @headers.setter
101 | def headers(self, value):
102 | val_dict = {}
103 | if value is not None:
104 | val_dict.update(value if isinstance(value, dict) else vars(value))
105 | self._headers = val_dict
106 |
107 | @property
108 | def query_params(self):
109 | """Query parameters for the HTTP connection ("registration", "since", "statementId", etc.)
110 |
111 | :setter: Accepts a dict or an object
112 | :setter type: dict
113 | :rtype: dict
114 | """
115 | return self._query_params
116 |
117 | @query_params.setter
118 | def query_params(self, value):
119 | val_dict = {}
120 | if value is not None:
121 | val_dict.update(value if isinstance(value, dict) else vars(value))
122 | self._query_params = val_dict
123 |
124 | @property
125 | def content(self):
126 | """Content body for the HTTP connection. Valid json string.
127 |
128 | :setter: Tries to convert to unicode
129 | :setter type: str | unicode
130 | :rtype: unicode
131 | """
132 | return self._content
133 |
134 | @content.setter
135 | def content(self, value):
136 | if not isinstance(value, str) and value is not None:
137 | value = value.decode("utf-8")
138 | self._content = value
139 |
140 | @content.deleter
141 | def content(self):
142 | del self._content
143 |
144 | @property
145 | def ignore404(self):
146 | """True if this request should consider a 404 response successful, False otherwise
147 |
148 | :setter: Tries to convert to boolean
149 | :setter type: bool
150 | :rtype: bool
151 | """
152 | return self._ignore404
153 |
154 | @ignore404.setter
155 | def ignore404(self, value):
156 | self._ignore404 = bool(value)
157 |
158 | @ignore404.deleter
159 | def ignore404(self):
160 | del self._ignore404
161 |
--------------------------------------------------------------------------------
/tincan/interaction_component.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.serializable_base import SerializableBase
16 | from tincan.language_map import LanguageMap
17 |
18 | """
19 | .. module:: interactioncomponent
20 | :synopsis: Provides action-level granularity to an object of a \
21 | statement. E.g. The text of a multiple choice question.
22 |
23 | """
24 |
25 |
26 | class InteractionComponent(SerializableBase):
27 | _props_req = [
28 | 'id',
29 | ]
30 |
31 | _props = [
32 | 'description',
33 | ]
34 |
35 | _props.extend(_props_req)
36 |
37 | def __init__(self, *args, **kwargs):
38 | self._id = None
39 | self._description = None
40 |
41 | super(InteractionComponent, self).__init__(*args, **kwargs)
42 |
43 | @property
44 | def id(self):
45 | """Id for Agent
46 |
47 | :setter: Tries to convert to unicode
48 | :setter type: unicode
49 | :rtype: unicode
50 |
51 | """
52 | return self._id
53 |
54 | @id.setter
55 | def id(self, value):
56 | if value is not None:
57 | if value == '':
58 | raise ValueError("id cannot be set to an empty string or non-string type")
59 | self._id = None if value is None else str(value)
60 |
61 | @property
62 | def description(self):
63 | """Description for Agent
64 |
65 | :setter: Tries to convert to :class:`tincan.LanguageMap`
66 | :setter type: :class:`tincan.LanguageMap`
67 | :rtype: :class:`tincan.LanguageMap`
68 |
69 | """
70 | return self._description
71 |
72 | @description.setter
73 | def description(self, value):
74 | if value is not None and not isinstance(value, LanguageMap):
75 | value = LanguageMap(value)
76 | self._description = value
77 |
78 | @description.deleter
79 | def description(self):
80 | del self._description
81 |
--------------------------------------------------------------------------------
/tincan/interaction_component_list.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.interaction_component import InteractionComponent
16 | from tincan.typed_list import TypedList
17 |
18 | """
19 | .. module:: interactioncomponentlist
20 | :synopsis: A wrapper for list that is able to type check
21 |
22 | """
23 |
24 |
25 | class InteractionComponentList(TypedList):
26 | _cls = InteractionComponent
27 |
--------------------------------------------------------------------------------
/tincan/language_map.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.serializable_base import SerializableBase
16 |
17 | """
18 | .. module:: languagemap
19 | :synopsis: A simple wrapper for a map containing language mappings
20 |
21 | """
22 |
23 |
24 | class LanguageMap(dict, SerializableBase):
25 | def __init__(self, *args, **kwargs):
26 | """Initializes a LanguageMap with the given mapping
27 |
28 | This constructor will first check the arguments for flatness
29 | to avoid nested languagemaps (which are invalid) and then
30 | call the base dict constructor
31 |
32 | """
33 | check_args = dict(*args, **kwargs)
34 | list(map(lambda k_v: (k_v[0], self._check_basestring(k_v[1])), iter(check_args.items())))
35 | super(LanguageMap, self).__init__(check_args)
36 |
37 | def __setitem__(self, prop, value):
38 | """Allows bracket notation for setting values with hyphenated keys
39 |
40 | :param prop: The property to set
41 | :type prop: str
42 | :param value: The value to set
43 | :type value: obj
44 |
45 | :raises: NameError, LanguageMapTypeError
46 |
47 | """
48 | self._check_basestring(value)
49 | super(LanguageMap, self).__setitem__(prop, value)
50 |
51 | @staticmethod
52 | def _check_basestring(value):
53 | """Ensures that value is an instance of basestring
54 |
55 | :param value: the value to check
56 | :type value: any
57 |
58 | """
59 | if not isinstance(value, str):
60 | raise TypeError("Value must be a stringstring_types")
61 |
--------------------------------------------------------------------------------
/tincan/lrs_response.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from http.client import HTTPResponse
16 |
17 | from tincan.http_request import HTTPRequest
18 | from tincan.base import Base
19 |
20 |
21 | class LRSResponse(Base):
22 | """Creates a new LRSResponse object, either from a dict, another object, or from kwargs
23 |
24 | :param success: True if the LRS return a successful status (sometimes includes 404), False otherwise
25 | :type success: bool
26 | :param request: HTTPRequest object that was sent to the LRS
27 | :type request: HTTPRequest
28 | :param response: HTTPResponse object that was received from the LRS
29 | :type response: HTTPResponse
30 | :param data: Body of the HTTPResponse
31 | :type data: unicode
32 | :param content: Parsed content received from the LRS
33 | """
34 |
35 | _props_req = [
36 | 'success',
37 | 'request',
38 | 'response',
39 | 'data',
40 | ]
41 |
42 | _props = [
43 | 'content',
44 | ]
45 |
46 | _props.extend(_props_req)
47 |
48 | def __init__(self, *args, **kwargs):
49 | self._success = None
50 | self._request = None
51 | self._response = None
52 | self._data = None
53 | self._content = None
54 |
55 | super(LRSResponse, self).__init__(*args, **kwargs)
56 |
57 | @property
58 | def success(self):
59 | """The LRSResponse's success. True if the LRS return a
60 | successful status (sometimes includes 404), False otherwise.
61 |
62 | :setter: Tries to convert to boolean
63 | :setter type: bool
64 | :rtype: bool
65 | """
66 | return self._success
67 |
68 | @success.setter
69 | def success(self, value):
70 | self._success = bool(value)
71 |
72 | @property
73 | def request(self):
74 | """The HTTPRequest object that was sent to the LRS
75 |
76 | :setter: Tries to convert to an HTTPRequest object
77 | :setter type: :class:`tincan.http_request.HTTPRequest`
78 | :rtype: :class:`tincan.http_request.HTTPRequest`
79 | """
80 | return self._request
81 |
82 | @request.setter
83 | def request(self, value):
84 | if value is not None and not isinstance(value, HTTPRequest):
85 | value = HTTPRequest(value)
86 |
87 | self._request = value
88 |
89 | @property
90 | def response(self):
91 | """The HTTPResponse object that was sent to the LRS
92 |
93 | :setter: Must be an HTTPResponse object
94 | :setter type: :class:`httplib.HTTPResponse`
95 | :rtype: :class:`httplib.HTTPResponse`
96 | """
97 | return self._response
98 |
99 | @response.setter
100 | def response(self, value):
101 | if value is not None and not isinstance(value, HTTPResponse):
102 | raise TypeError(
103 | f"Property 'response' in 'tincan.{self.__class__.__name__}' must be set with an HTTPResponse object"
104 | )
105 | self._response = value
106 |
107 | @property
108 | def data(self):
109 | return self._data
110 |
111 | @data.setter
112 | def data(self, value):
113 | """Setter for the _data attribute. Should be set from response.read()
114 |
115 | :param value: The body of the response object for the LRSResponse
116 | :type value: unicode
117 | """
118 | if value is not None and isinstance(value, (bytes, bytearray)):
119 | value = value.decode('utf-8')
120 | self._data = value
121 |
122 | @property
123 | def content(self):
124 | """Parsed content received from the LRS
125 | """
126 | return self._content
127 |
128 | @content.setter
129 | def content(self, value):
130 | self._content = value
131 |
132 | @content.deleter
133 | def content(self):
134 | del self._content
135 |
--------------------------------------------------------------------------------
/tincan/score.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.serializable_base import SerializableBase
16 |
17 |
18 | class Score(SerializableBase):
19 | """Stores the scoring data for an activity.
20 |
21 | Can be created from a dict, another Score, or from kwargs.
22 |
23 | All these attributes are optional and settable to None:
24 |
25 | :param scaled: Scaled score
26 | :type scaled: float
27 | :param raw: Raw score
28 | :type raw: float
29 | :param min: Minimum score possible
30 | :type min: float
31 | :param max: Maximum score possible
32 | :type max: float
33 | """
34 |
35 | _props = [
36 | 'scaled',
37 | 'raw',
38 | 'min',
39 | 'max',
40 | ]
41 |
42 | def __init__(self, *args, **kwargs):
43 | self._scaled = None
44 | self._raw = None
45 | self._min = None
46 | self._max = None
47 |
48 | super(SerializableBase, self).__init__(*args, **kwargs)
49 |
50 | @property
51 | def scaled(self):
52 | """Scaled for Score
53 |
54 | :setter: Tries to convert to float. If None is provided,
55 | this signifies the absence of this data.
56 | :setter type: float | int | None
57 | :rtype: float | None
58 | :raises: TypeError if unsupported type is provided
59 | """
60 | return self._scaled
61 |
62 | @scaled.setter
63 | def scaled(self, value):
64 | if value is None or isinstance(value, float):
65 | self._scaled = value
66 | return
67 | try:
68 | self._scaled = float(value)
69 | except Exception as e:
70 | msg = (
71 | f"Property 'scaled' in a 'tincan.{self.__class__.__name__}' object must be set with a "
72 | f"float or None."
73 | )
74 | msg += repr(e)
75 | raise TypeError(msg)
76 |
77 | @scaled.deleter
78 | def scaled(self):
79 | del self._scaled
80 |
81 | @property
82 | def raw(self):
83 | """Raw for Score
84 |
85 | :setter: Tries to convert to float. If None is provided,
86 | this signifies the absence of this data.
87 | :setter type: float | int | None
88 | :rtype: float | None
89 | :raises: TypeError if unsupported type is provided
90 | """
91 | return self._raw
92 |
93 | @raw.setter
94 | def raw(self, value):
95 | if value is None or isinstance(value, float):
96 | self._raw = value
97 | return
98 | try:
99 | self._raw = float(value)
100 | except Exception as e:
101 | msg = (
102 | f"Property 'raw' in a 'tincan.{self.__class__.__name__}' object must be set with a "
103 | f"float or None."
104 | )
105 | msg += repr(e)
106 | raise TypeError(msg)
107 |
108 | @raw.deleter
109 | def raw(self):
110 | del self._raw
111 |
112 | @property
113 | def min(self):
114 | """Min for Score
115 |
116 | :setter: Tries to convert to float. If None is provided,
117 | this signifies the absence of this data.
118 | :setter type: float | int | None
119 | :rtype: float | None
120 | :raises: TypeError if unsupported type is provided
121 | """
122 | return self._min
123 |
124 | @min.setter
125 | def min(self, value):
126 | if value is None or isinstance(value, float):
127 | self._min = value
128 | return
129 | try:
130 | self._min = float(value)
131 | except Exception as e:
132 | msg = (
133 | f"Property 'min' in a 'tincan.{self.__class__.__name__}' object must be set with a "
134 | f"float or None."
135 | )
136 | msg += repr(e)
137 | raise TypeError(msg)
138 |
139 | @min.deleter
140 | def min(self):
141 | del self._min
142 |
143 | @property
144 | def max(self):
145 | """Max for Score
146 |
147 | :setter: Tries to convert to float. If None is provided,
148 | this signifies the absence of this data.
149 | :setter type: float | int | None
150 | :rtype: float | None
151 | :raises: TypeError if unsupported type is provided
152 | """
153 | return self._max
154 |
155 | @max.setter
156 | def max(self, value):
157 | if value is None or isinstance(value, float):
158 | self._max = value
159 | return
160 | try:
161 | self._max = float(value)
162 | except Exception as e:
163 | msg = (
164 | f"Property 'max' in a 'tincan.{self.__class__.__name__}' object must be set with a "
165 | f"float or None."
166 | )
167 | msg += repr(e)
168 | raise TypeError(msg)
169 |
170 | @max.deleter
171 | def max(self):
172 | del self._max
173 |
--------------------------------------------------------------------------------
/tincan/serializable_base.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import json
16 | import uuid
17 | import datetime
18 | import re
19 |
20 | from tincan.base import Base
21 | from tincan.version import Version
22 | from tincan.conversions.iso8601 import jsonify_datetime, jsonify_timedelta
23 |
24 |
25 | """
26 | .. module:: serializable_base
27 | :synopsis: A base object that provides the common initializer from :class:`tincan.Base`
28 | as well as common serialization functionality
29 |
30 | """
31 |
32 |
33 | class SerializableBase(Base):
34 | _props_corrected = {
35 | '_more_info': 'moreInfo',
36 | '_interaction_type': 'interactionType',
37 | '_correct_responses_pattern': 'correctResponsesPattern',
38 | '_object_type': 'objectType',
39 | '_usage_type': 'usageType',
40 | '_content_type': 'contentType',
41 | '_fileurl': 'fileUrl',
42 | '_context_activities': 'contextActivities',
43 | '_home_page': 'homePage',
44 | }
45 |
46 | _UUID_REGEX = re.compile(
47 | r'^[a-f0-9]{8}-'
48 | r'[a-f0-9]{4}-'
49 | r'[1-5][a-f0-9]{3}-'
50 | r'[89ab][a-f0-9]{3}-'
51 | r'[a-f0-9]{12}$'
52 | )
53 |
54 | def __init__(self, *args, **kwargs):
55 |
56 | new_kwargs = {}
57 | for obj in args:
58 | new_kwargs.update(obj if isinstance(obj, dict) else vars(obj))
59 |
60 | new_kwargs.update(kwargs)
61 |
62 | for uscore, camel in self._props_corrected.items():
63 | if camel in new_kwargs:
64 | new_kwargs[uscore[1:]] = new_kwargs[camel]
65 | new_kwargs.pop(camel)
66 |
67 | super(SerializableBase, self).__init__(**new_kwargs)
68 |
69 | @classmethod
70 | def from_json(cls, json_data):
71 | """Tries to convert a JSON representation to an object of the same
72 | type as self
73 |
74 | A class can provide a _fromJSON implementation in order to do specific
75 | type checking or other custom implementation details. This method
76 | will throw a ValueError for invalid JSON, a TypeError for
77 | improperly constructed, but valid JSON, and any custom errors
78 | that can be be propagated from class constructors.
79 |
80 | :param json_data: The JSON string to convert
81 | :type json_data: str | unicode
82 |
83 | :raises: TypeError, ValueError, LanguageMapInitError
84 | """
85 |
86 | data = json.loads(json_data)
87 | result = cls(data)
88 | if hasattr(result, "_from_json"):
89 | result._from_json()
90 | return result
91 |
92 | def to_json(self, version=Version.latest):
93 | """Tries to convert an object into a JSON representation and return
94 | the resulting string
95 |
96 | An Object can define how it is serialized by overriding the as_version()
97 | implementation. A caller may further define how the object is serialized
98 | by passing in a custom encoder. The default encoder will ignore
99 | properties of an object that are None at the time of serialization.
100 |
101 | :param version: The version to which the object must be serialized to.
102 | This will default to the latest version supported by the library.
103 | :type version: str | unicode
104 |
105 | """
106 | return json.dumps(self.as_version(version))
107 |
108 | def as_version(self, version=Version.latest):
109 | """Returns a dict that has been modified based on versioning
110 | in order to be represented in JSON properly
111 |
112 | A class should overload as_version(self, version)
113 | implementation in order to tailor a more specific representation
114 |
115 | :param version: the relevant version. This allows for variance
116 | between versions
117 | :type version: str | unicode
118 |
119 | """
120 | if not isinstance(self, list):
121 | result = {}
122 | for k, v in iter(self.items()) if isinstance(self, dict) else iter(vars(self).items()):
123 | k = self._props_corrected.get(k, k)
124 | if isinstance(v, SerializableBase):
125 | result[k] = v.as_version(version)
126 | elif isinstance(v, list):
127 | result[k] = []
128 | for val in v:
129 | if isinstance(val, SerializableBase):
130 | result[k].append(val.as_version(version))
131 | else:
132 | result[k].append(val)
133 | elif isinstance(v, uuid.UUID):
134 | result[k] = str(v)
135 | elif isinstance(v, datetime.timedelta):
136 | result[k] = jsonify_timedelta(v)
137 | elif isinstance(v, datetime.datetime):
138 | result[k] = jsonify_datetime(v)
139 | else:
140 | result[k] = v
141 | result = self._filter_none(result)
142 | else:
143 | result = []
144 | for v in self:
145 | if isinstance(v, SerializableBase):
146 | result.append(v.as_version(version))
147 | else:
148 | result.append(v)
149 | return result
150 |
151 | @staticmethod
152 | def _filter_none(obj):
153 | """Filters out attributes set to None prior to serialization, and
154 | returns a new object without those attributes. This saves
155 | the serializer from sending empty bytes over the network. This method also
156 | fixes the keys to look as expected by ignoring a leading '_' if it
157 | is present.
158 |
159 | :param obj: the dictionary representation of an object that may have
160 | None attributes
161 | :type obj: dict
162 |
163 | """
164 | result = {}
165 | for k, v in obj.items():
166 | if v is not None:
167 | if k.startswith('_'):
168 | k = k[1:]
169 | result[k] = v
170 | return result
171 |
--------------------------------------------------------------------------------
/tincan/statement_base.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from datetime import datetime
16 |
17 | from tincan.serializable_base import SerializableBase
18 | from tincan.agent import Agent
19 | from tincan.group import Group
20 | from tincan.verb import Verb
21 | from tincan.context import Context
22 | from tincan.attachment import Attachment
23 | from tincan.attachment_list import AttachmentList
24 | from tincan.conversions.iso8601 import make_datetime
25 |
26 |
27 | """
28 |
29 | .. module:: StatementBase
30 | :synopsis: The base object for both Statement and SubStatement
31 |
32 | """
33 |
34 |
35 | class StatementBase(SerializableBase):
36 | _props_req = [
37 | 'actor',
38 | 'verb',
39 | 'object',
40 | 'timestamp',
41 | ]
42 |
43 | _props = [
44 | 'context',
45 | 'attachments'
46 | ]
47 |
48 | _props.extend(_props_req)
49 |
50 | def __init__(self, *args, **kwargs):
51 | self._actor = None
52 | self._verb = None
53 | self._object = None
54 | self._timestamp = None
55 | self._context = None
56 | self._attachments = None
57 |
58 | super(StatementBase, self).__init__(*args, **kwargs)
59 |
60 | @property
61 | def actor(self):
62 | """Actor for StatementBase
63 |
64 | :setter: Tries to convert to :class:`tincan.Agent` or :class:`tincan.Group`
65 | :setter type: :class:`tincan.Agent` | :class:`tincan.Group`
66 | :rtype: :class:`tincan.Agent` | :class:`tincan.Group`
67 |
68 | """
69 | return self._actor
70 |
71 | @actor.setter
72 | def actor(self, value):
73 | if value is not None and not isinstance(value, Agent) and not isinstance(value, Group):
74 | if isinstance(value, dict):
75 | if 'object_type' in value or 'objectType' in value:
76 | if 'objectType' in value:
77 | value['object_type'] = value['objectType']
78 | value.pop('objectType')
79 | if value['object_type'] == 'Agent':
80 | value = Agent(value)
81 | elif value['object_type'] == 'Group':
82 | value = Group(value)
83 | else:
84 | value = Agent(value)
85 | else:
86 | value = Agent(value)
87 | self._actor = value
88 |
89 | @actor.deleter
90 | def actor(self):
91 | del self._actor
92 |
93 | @property
94 | def verb(self):
95 | """Verb for StatementBase
96 |
97 | :setter: Tries to convert to :class:`tincan.Verb`
98 | :setter type: :class:`tincan.Verb`
99 | :rtype: :class:`tincan.Verb`
100 |
101 | """
102 | return self._verb
103 |
104 | @verb.setter
105 | def verb(self, value):
106 | if value is not None and not isinstance(value, Verb):
107 | value = Verb(value)
108 | self._verb = value
109 |
110 | @verb.deleter
111 | def verb(self):
112 | del self._verb
113 |
114 | @property
115 | def timestamp(self):
116 | """Timestamp for StatementBase
117 |
118 | :setter: Tries to convert to :class:`datetime.datetime`. If
119 | no timezone is given, makes a naive `datetime.datetime`.
120 |
121 | Strings will be parsed as ISO 8601 timestamps.
122 |
123 | If a number is provided, it will be interpreted as a UNIX
124 | timestamp, which by definition is UTC.
125 |
126 | If a `dict` is provided, does `datetime.datetime(**value)`.
127 |
128 | If a `tuple` or a `list` is provided, does
129 | `datetime.datetime(*value)`. Uses the timezone in the tuple or
130 | list if provided.
131 |
132 | :setter type: :class:`datetime.datetime` | unicode | str | int | float | dict | tuple | list | None
133 | :rtype: :class:`datetime.datetime`
134 | """
135 | return self._timestamp
136 |
137 | @timestamp.setter
138 | def timestamp(self, value):
139 | if value is None or isinstance(value, datetime):
140 | self._timestamp = value
141 | return
142 |
143 | try:
144 | self._timestamp = make_datetime(value)
145 | except TypeError as e:
146 | message = (
147 | f"Property 'timestamp' in a 'tincan.{self.__class__.__name__}' "
148 | f"object must be set with a "
149 | f"datetime.datetime, str, unicode, int, float, dict "
150 | f"or None.\n\n{repr(e)}"
151 | )
152 | raise TypeError(message) from e
153 |
154 | @timestamp.deleter
155 | def timestamp(self):
156 | del self._timestamp
157 |
158 | @property
159 | def context(self):
160 | """Context for StatementBase
161 |
162 | :setter: Tries to convert to :class:`tincan.Context`
163 | :setter type: :class:`tincan.Context`
164 | :rtype: :class:`tincan.Context`
165 |
166 | """
167 | return self._context
168 |
169 | @context.setter
170 | def context(self, value):
171 | if value is not None and not isinstance(value, Context):
172 | value = Context(value)
173 | self._context = value
174 |
175 | @context.deleter
176 | def context(self):
177 | del self._context
178 |
179 | @property
180 | def attachments(self):
181 | """Attachments for StatementBase
182 |
183 | :setter: Tries to convert each element to :class:`tincan.Attachment`
184 | :setter type: :class:`tincan.AttachmentList`
185 | :rtype: :class:`tincan.AttachmentList`
186 |
187 | """
188 | return self._attachments
189 |
190 | @attachments.setter
191 | def attachments(self, value):
192 | if value is not None and not isinstance(value, AttachmentList):
193 | try:
194 | value = AttachmentList([Attachment(value)])
195 | except (TypeError, AttributeError):
196 | value = AttachmentList(value)
197 | self._attachments = value
198 |
199 | @attachments.deleter
200 | def attachments(self):
201 | del self._attachments
202 |
--------------------------------------------------------------------------------
/tincan/statement_list.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.statement import Statement
16 | from tincan.typed_list import TypedList
17 |
18 | """
19 | .. module:: statement_list
20 | :synopsis: A type checked Statement list
21 |
22 | """
23 |
24 |
25 | class StatementList(TypedList):
26 | _cls = Statement
27 |
--------------------------------------------------------------------------------
/tincan/statement_ref.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import uuid
16 |
17 | from tincan.serializable_base import SerializableBase
18 |
19 |
20 | """
21 | .. module StatementRef
22 | :synopsis: A StatementRef object that is a reference to another pre-existing statement.
23 | """
24 |
25 |
26 | class StatementRef(SerializableBase):
27 | _props_req = [
28 | 'object_type'
29 | ]
30 |
31 | _props = [
32 | 'id'
33 | ]
34 |
35 | _props.extend(_props_req)
36 |
37 | def __init__(self, *args, **kwargs):
38 | self._object_type = None
39 | self._id = None
40 |
41 | super(StatementRef, self).__init__(*args, **kwargs)
42 |
43 | @property
44 | def object_type(self):
45 | """Object type for Statement Ref. Will always be "StatementRef"
46 |
47 | :setter: Tries to convert to unicode
48 | :setter type: unicode
49 | :rtype: unicode
50 |
51 | """
52 | return self._object_type
53 |
54 | @object_type.setter
55 | def object_type(self, _):
56 | self._object_type = 'StatementRef'
57 |
58 | @property
59 | def id(self):
60 | """Id for Statement Ref
61 |
62 | :setter: Tries to convert to unicode
63 | :setter type: unicode
64 | :rtype: unicode
65 |
66 | """
67 | return self._id
68 |
69 | @id.setter
70 | def id(self, value):
71 | if value is not None and not isinstance(value, uuid.UUID):
72 | if isinstance(value, str) and not self._UUID_REGEX.match(value):
73 | raise ValueError("Invalid UUID string")
74 | value = uuid.UUID(value)
75 | self._id = value
76 |
77 | @id.deleter
78 | def id(self):
79 | del self._id
80 |
--------------------------------------------------------------------------------
/tincan/statement_targetable.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """
16 | .. module:: statement_targetable
17 | :synopsis: Provides a way to define objects as targetable by a statement
18 |
19 | At this time, objects that are targetable need access to only the \
20 | object type, but this may change in the future
21 |
22 | """
23 |
24 |
25 | class StatementTargetable(object):
26 | def __init__(self):
27 | self.object_type = None
28 |
29 | def get_object_type(self):
30 | """Returns the object type of self [Activity | Agent | \
31 | StatementRef | SubStatement]
32 | """
33 | return self.object_type
34 |
--------------------------------------------------------------------------------
/tincan/statements_result.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.serializable_base import SerializableBase
16 | from tincan.statement_list import StatementList
17 |
18 | """
19 | .. module:: statements_result
20 | :synopsis: Statements result model class, returned by LRS calls to get
21 | multiple statements.
22 | """
23 |
24 |
25 | class StatementsResult(SerializableBase):
26 | _props_req = [
27 | 'statements',
28 | 'more',
29 | ]
30 |
31 | _props = []
32 | _props.extend(_props_req)
33 |
34 | def __init__(self, *args, **kwargs):
35 | self._statements = None
36 | self._more = None
37 |
38 | super(StatementsResult, self).__init__(*args, **kwargs)
39 |
40 | @property
41 | def statements(self):
42 | """Statements for StatementsResult
43 |
44 | :setter: Tries to convert each element to :class:`tincan.Statement`
45 | :setter type: list of :class:`tincan.Statement`
46 | :rtype: list of :class:`tincan.Statement`
47 |
48 | """
49 | return self._statements
50 |
51 | @statements.setter
52 | def statements(self, value):
53 | if value is None:
54 | self._statements = StatementList()
55 | return
56 | try:
57 | self._statements = StatementList(value)
58 | except Exception:
59 | raise TypeError(f"Property 'statements' in a 'tincan.{self.__class__.__name__}' object must be set with a "
60 | f"list or None."
61 | f"\n\n"
62 | f"Tried to set it with a '{value.__class__.__name__}' object: {repr(value)}"
63 | f"\n\n")
64 |
65 |
66 | @property
67 | def more(self):
68 | """More for StatementsResult
69 |
70 | :setter: Tries to convert to unicode
71 | :setter type: unicode
72 | :rtype: unicode
73 |
74 | """
75 | return self._more
76 |
77 | @more.setter
78 | def more(self, value):
79 | if value is None or isinstance(value, str):
80 | self._more = value
81 | return
82 | try:
83 | self._more = str(value)
84 | except Exception as e:
85 | msg = (
86 | f"Property 'more' in a 'tincan.{self.__class__.__name__}' object must be set with a "
87 | f"str or None."
88 | )
89 | msg += repr(e)
90 | raise TypeError(msg) from e
91 |
--------------------------------------------------------------------------------
/tincan/substatement.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.statement_base import StatementBase
16 | from tincan.agent import Agent
17 | from tincan.group import Group
18 | from tincan.activity import Activity
19 |
20 |
21 | class SubStatement(StatementBase):
22 | _props_req = [
23 | 'object_type'
24 | ]
25 |
26 | _props = []
27 |
28 | _props.extend(StatementBase._props)
29 | _props.extend(_props_req)
30 |
31 | def __init__(self, *args, **kwargs):
32 | self._object_type = None
33 |
34 | super(SubStatement, self).__init__(*args, **kwargs)
35 |
36 | @property
37 | def object(self):
38 | """Object for SubStatement
39 |
40 | :setter: Setter for object
41 | :setter type: :class:`tincan.Agent` | :class:`tincan.Group` | :class:`tincan.Activity`
42 | :rtype: :class:`tincan.Agent` | :class:`tincan.Group` | :class:`tincan.Activity`
43 |
44 | """
45 | return self._object
46 |
47 | @object.setter
48 | def object(self, value):
49 | if value is not None and \
50 | not isinstance(value, Agent) and \
51 | not isinstance(value, Group) and \
52 | not isinstance(value, Activity):
53 | if isinstance(value, dict):
54 | if 'object_type' in value or 'objectType' in value:
55 | if 'objectType' in value:
56 | value['object_type'] = value['objectType']
57 | value.pop('objectType')
58 | if value['object_type'] == 'Agent':
59 | value = Agent(value)
60 | elif value['object_type'] == 'Activity':
61 | value = Activity(value)
62 | elif value['object_type'] == 'Group':
63 | value = Group(value)
64 | else:
65 | value = Activity(value)
66 | else:
67 | value = Activity(value)
68 | self._object = value
69 |
70 | @object.deleter
71 | def object(self):
72 | del self._object
73 |
74 | @property
75 | def object_type(self):
76 | """Object Type for SubStatement. Will always be "SubStatement"
77 |
78 | :setter: Tries to convert to unicode
79 | :setter type: unicode
80 | :rtype: unicode
81 |
82 | """
83 | return self._object_type
84 |
85 | @object_type.setter
86 | def object_type(self, _):
87 | self._object_type = 'SubStatement'
88 |
--------------------------------------------------------------------------------
/tincan/typed_list.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.serializable_base import SerializableBase
16 |
17 | """
18 | .. module:: typed_list
19 | :synopsis: A wrapper for a list that ensures the list consists of only one type
20 |
21 | """
22 |
23 |
24 | class TypedList(list, SerializableBase):
25 | _cls = None
26 |
27 | def __init__(self, *args, **kwargs):
28 | self._check_cls()
29 | new_args = [self._make_cls(v) for v in list(*args, **kwargs)]
30 | super(TypedList, self).__init__(new_args)
31 |
32 | def __setitem__(self, ind, value):
33 | self._check_cls()
34 | value = self._make_cls(value)
35 | super(TypedList, self).__setitem__(ind, value)
36 |
37 | def _check_cls(self):
38 | """If self._cls is not set, raises ValueError.
39 |
40 | :raises: ValueError
41 | """
42 | if self._cls is None:
43 | raise ValueError("_cls has not been set")
44 |
45 | def _make_cls(self, value):
46 | """If value is not instance of self._cls, converts and returns
47 | it. Otherwise, returns value.
48 |
49 | :param value: the thing to make a self._cls from
50 | :rtype self._cls
51 | """
52 | if isinstance(value, self._cls):
53 | return value
54 | return self._cls(value)
55 |
56 | def append(self, value):
57 | self._check_cls()
58 | value = self._make_cls(value)
59 | super(TypedList, self).append(value)
60 |
61 | def extend(self, value):
62 | self._check_cls()
63 | new_args = [self._make_cls(v) for v in value]
64 | super(TypedList, self).extend(new_args)
65 |
66 | def insert(self, ind, value):
67 | self._check_cls()
68 | value = self._make_cls(value)
69 | super(TypedList, self).insert(ind, value)
70 |
--------------------------------------------------------------------------------
/tincan/verb.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from tincan.serializable_base import SerializableBase
16 | from tincan.language_map import LanguageMap
17 |
18 | """
19 | .. module:: verb
20 | :synopsis: A Verb object that contains an id and a display
21 |
22 | """
23 |
24 |
25 | class Verb(SerializableBase):
26 | _props_req = [
27 | 'id',
28 | ]
29 |
30 | _props = [
31 | 'display',
32 | ]
33 |
34 | _props.extend(_props_req)
35 |
36 | def __init__(self, *args, **kwargs):
37 | self._id = None
38 | self._display = None
39 |
40 | super(Verb, self).__init__(*args, **kwargs)
41 |
42 | def __repr__(self):
43 | return f'Verb: {self.__dict__}'
44 |
45 | @property
46 | def id(self):
47 | """Id for Verb
48 |
49 | :setter: Tries to convert to unicode
50 | :setter type: unicode
51 | :rtype: unicode
52 |
53 | """
54 | return self._id
55 |
56 | @id.setter
57 | def id(self, value):
58 | if value is not None:
59 | if value == '':
60 | raise ValueError(
61 | f"Property 'id' in 'tincan.{self.__class__.__name__}' object must be not empty."
62 | )
63 | self._id = None if value is None else str(value)
64 |
65 | @property
66 | def display(self):
67 | """Display for Verb
68 |
69 | :setter: Tries to convert to :class:`tincan.LanguageMap`
70 | :setter type: :class:`tincan.LanguageMap`
71 | :rtype: :class:`tincan.LanguageMap`
72 |
73 | """
74 | return self._display
75 |
76 | @display.setter
77 | def display(self, value):
78 | if value is not None and not isinstance(value, LanguageMap):
79 | value = LanguageMap(value)
80 | self._display = value
81 |
82 | @display.deleter
83 | def display(self):
84 | del self._display
85 |
--------------------------------------------------------------------------------
/tincan/version.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 Rustici Software
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """
16 | .. module:: version
17 | :synopsis: A class to provide versioning information to other modules
18 |
19 | """
20 |
21 |
22 | class Version(object):
23 | supported = [
24 | '1.0.3',
25 | '1.0.2',
26 | '1.0.1',
27 | '1.0.0',
28 | ]
29 | latest = supported[0]
30 |
--------------------------------------------------------------------------------