├── .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 | [![Build Status](https://travis-ci.org/RusticiSoftware/TinCanPython.png)](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 | --------------------------------------------------------------------------------