14 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### Release 2.0.0
2 | * Upgrade Sample App version from 2.0.0-rc.1 to 2.0.0
3 |
4 | ### Release 2.0.0-rc.1
5 | * Upgraded Pyqldb version from 2.0.0 to 3.0.0-rc.1
6 |
7 | ### Release 1.0.0
8 |
9 | * Upgraded pyqldb version from v1.0.0-rc.2 to 2.0.0
10 | * Used args for execute_statement instead of a list
11 | * Added examples for native python data types
12 |
13 | ### Release 1.0.0-rc.2
14 |
15 | * Fixes for small documentation issues.
16 |
17 | ### Release 1.0.0-rc.1
18 |
19 | * Initial preview release of the QLDB Python Sample Application.
20 |
21 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
4 | # the License. A copy of the License is located at
5 | #
6 | # http://www.apache.org/licenses/LICENSE-2.0
7 | #
8 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
9 | # CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
10 | # and limitations under the License.
11 |
--------------------------------------------------------------------------------
/docs/source/_templates/layout.html:
--------------------------------------------------------------------------------
1 | {%- extends "!layout.html" %}
2 |
3 | {%- block breadcrumbs %}
4 | {{ super() }}
5 |
6 | {%- endblock %}
7 |
8 |
9 | {%- block footer %}
10 | {{ super() }}
11 |
12 |
20 | {%- endblock %}
21 |
--------------------------------------------------------------------------------
/docs/source/_static/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Page Not Found
6 |
30 |
31 |
32 |
Page Not Found
33 |
Sorry, the page you requested could not be found.
34 |
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | SPDX-License-Identifier: MIT-0
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | software and associated documentation files (the "Software"), to deal in the Software
6 | without restriction, including without limitation the rights to use, copy, modify,
7 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so.
9 |
10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. AmazonQLDB Sample App documentation master file, created by
2 | sphinx-quickstart on Mon Oct 7 18:23:00 2019.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | AmazonQLDB Python Sample App Documentation
7 | ===================================
8 | This sample app demonstrates several uses of Amazon Quantum Ledger Database (QLDB).
9 |
10 |
11 | Quickstart
12 | ----------
13 |
14 | .. toctree::
15 | :maxdepth: 2
16 |
17 | guide/quickstart
18 |
19 |
20 | API Reference
21 | -------------
22 |
23 | Core
24 | ~~~~
25 |
26 | .. toctree::
27 | :maxdepth: 3
28 |
29 | reference/core/index
30 |
31 | Model
32 | ~~~~~
33 |
34 | .. toctree::
35 | :maxdepth: 3
36 |
37 | reference/model/sample_data
38 |
39 | QLDB
40 | ~~~~
41 |
42 | .. toctree::
43 | :maxdepth: 3
44 |
45 | reference/qldb/index
46 |
47 |
48 | Indices and tables
49 | ==================
50 |
51 | * :ref:`genindex`
52 | * :ref:`modindex`
53 | * :ref:`search`
54 |
55 |
--------------------------------------------------------------------------------
/pyqldbsamples/model/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 |
--------------------------------------------------------------------------------
/pyqldbsamples/qldb/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
4 | # the License. A copy of the License is located at
5 | #
6 | # http://www.apache.org/licenses/LICENSE-2.0
7 | #
8 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
9 | # CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
10 | # and limitations under the License.
11 | import pytest
12 |
13 |
14 | def pytest_addoption(parser):
15 | # Collect config values from cmd line or setup.cfg
16 | parser.addoption(
17 | "--ledger_suffix", action="store", default="", help=""
18 | )
19 |
20 |
21 | @pytest.fixture(scope='class', autouse=True)
22 | def config_variables(request):
23 | # Set as class attribute on the invoking test context.
24 | request.cls.ledger_suffix = request.config.getoption("--ledger_suffix").replace(".", "-")
25 |
--------------------------------------------------------------------------------
/pyqldbsamples/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 |
17 | __version__ = '2.0.0'
18 |
--------------------------------------------------------------------------------
/pyqldbsamples/qldb/block_address.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 |
17 |
18 | def block_address_to_dictionary(ion_dict):
19 | """
20 | Convert a block address from IonPyDict into a dictionary.
21 | Shape of the dictionary must be: {'IonText': "{strandId: <"strandId">, sequenceNo: }"}
22 |
23 | :type ion_dict: :py:class:`amazon.ion.simple_types.IonPyDict`/str
24 | :param ion_dict: The block address value to convert.
25 |
26 | :rtype: dict
27 | :return: The converted dict.
28 | """
29 | block_address = {'IonText': {}}
30 | if not isinstance(ion_dict, str):
31 | py_dict = '{{strandId: "{}", sequenceNo:{}}}'.format(ion_dict['strandId'], ion_dict['sequenceNo'])
32 | ion_dict = py_dict
33 | block_address['IonText'] = ion_dict
34 | return block_address
35 |
--------------------------------------------------------------------------------
/pyqldbsamples/constants.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 |
17 |
18 | class Constants:
19 | """
20 | Constant values used throughout this tutorial.
21 | """
22 | LEDGER_NAME = "vehicle-registration"
23 |
24 | VEHICLE_REGISTRATION_TABLE_NAME = "VehicleRegistration"
25 | VEHICLE_TABLE_NAME = "Vehicle"
26 | PERSON_TABLE_NAME = "Person"
27 | DRIVERS_LICENSE_TABLE_NAME = "DriversLicense"
28 |
29 | LICENSE_NUMBER_INDEX_NAME = "LicenseNumber"
30 | GOV_ID_INDEX_NAME = "GovId"
31 | VEHICLE_VIN_INDEX_NAME = "VIN"
32 | LICENSE_PLATE_NUMBER_INDEX_NAME = "LicensePlateNumber"
33 | PERSON_ID_INDEX_NAME = "PersonId"
34 |
35 | JOURNAL_EXPORT_S3_BUCKET_NAME_PREFIX = "qldb-tutorial-journal-export"
36 | USER_TABLES = "information_schema.user_tables"
37 | S3_BUCKET_ARN_TEMPLATE = "arn:aws:s3:::"
38 | LEDGER_NAME_WITH_TAGS = "tags"
39 |
40 | RETRY_LIMIT = 4
41 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | import os
17 | import re
18 | import setuptools
19 |
20 | ROOT = os.path.join(os.path.dirname(__file__), 'pyqldbsamples')
21 | VERSION_RE = re.compile(r'''__version__ = ['"]([0-9.a-z\-]+)['"]''')
22 | requires = ['amazon.ion>=0.7.0,<1',
23 | 'boto3>=1.16.56,<2',
24 | 'botocore>=1.19.56,<2',
25 | 'pyqldb>=3.1.0,<4'
26 | ]
27 |
28 |
29 | def get_version():
30 | init = open(os.path.join(ROOT, '__init__.py')).read()
31 | return VERSION_RE.search(init).group(1)
32 |
33 |
34 | setuptools.setup(
35 | name='pyqldbsamples',
36 | version=get_version(),
37 | description='Sample app for Amazon QLDB',
38 | long_description=open('README.md').read(),
39 | long_description_content_type='text/markdown',
40 | author='Amazon Web Services',
41 | packages=setuptools.find_packages(),
42 | install_requires=requires,
43 | license="Apache License 2.0"
44 | )
45 |
--------------------------------------------------------------------------------
/docs/source/guide/quickstart.rst:
--------------------------------------------------------------------------------
1 | .. _guide_quickstart:
2 |
3 | Quickstart
4 | ==========
5 | Getting started with AmazonQLDB sample application is easy, but requires a few steps.
6 |
7 | Installation
8 | ------------
9 | Install the Python AmazonQLDB driver and other dependencies via :command:`pip`::
10 |
11 | pip install -r requirements.txt
12 |
13 | Configuration
14 | -------------
15 | Before you can begin running the sample application, you should set up authentication
16 | credentials. Credentials for your AWS account can be found in the
17 | `IAM Console `_. You can
18 | create or use an existing user. Go to manage access keys and
19 | generate a new set of keys.
20 |
21 | If you have the `AWS CLI `_
22 | installed, then you can use it to configure your credentials file::
23 |
24 | aws configure
25 |
26 | Alternatively, you can create the credential file yourself. By default,
27 | its location is at ``~/.aws/credentials``::
28 |
29 | [default]
30 | aws_access_key_id = YOUR_ACCESS_KEY
31 | aws_secret_access_key = YOUR_SECRET_KEY
32 |
33 | You may also want to set a default region. This can be done in the
34 | configuration file. By default, its location is at ``~/.aws/config``::
35 |
36 | [default]
37 | region=us-east-1
38 |
39 | Alternatively, you can pass a ``region_name`` when creating the driver.
40 |
41 | This sets up credentials for the default profile as well as a default
42 | region to use when creating connections.
43 |
44 | Using Sample Application
45 | ------------------------
46 |
47 | The sample code creates a ledger with tables and indexes, and inserts some documents into those tables,
48 | among other things. Each of the files in the sample app can be run in the following way::
49 |
50 | python create_ledger.py
51 |
52 | The above example will build the CreateLedger class with the necessary dependencies and create a ledger named:
53 | ``vehicle-registration``. You may run other examples after creating a ledger.
54 |
55 |
--------------------------------------------------------------------------------
/pyqldbsamples/list_ledgers.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 |
21 | from boto3 import client
22 |
23 | logger = getLogger(__name__)
24 | basicConfig(level=INFO)
25 | qldb_client = client('qldb')
26 |
27 |
28 | def list_ledgers():
29 | """
30 | List all ledgers.
31 |
32 | :rtype: list
33 | :return: List of ledgers.
34 | """
35 | logger.info("Let's list all the ledgers...")
36 | result = qldb_client.list_ledgers()
37 | ledgers = result.get('Ledgers')
38 | logger.info('Success. List of ledgers: {}.'.format(ledgers))
39 | return ledgers
40 |
41 |
42 | def main():
43 | """
44 | List all QLDB ledgers in a given account.
45 | """
46 | try:
47 | list_ledgers()
48 | except Exception as e:
49 | logger.exception('Unable to list ledgers!')
50 | raise e
51 |
52 |
53 | if __name__ == '__main__':
54 | main()
55 |
--------------------------------------------------------------------------------
/pyqldbsamples/describe_ledger.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 |
21 | from boto3 import client
22 |
23 | from pyqldbsamples.constants import Constants
24 |
25 | logger = getLogger(__name__)
26 | basicConfig(level=INFO)
27 | qldb_client = client('qldb')
28 |
29 |
30 | def describe_ledger(ledger_name):
31 | """
32 | Describe a ledger.
33 |
34 | :type ledger_name: str
35 | :param ledger_name: Name of the ledger to describe.
36 | """
37 | logger.info('describe ledger with name: {}.'.format(ledger_name))
38 | result = qldb_client.describe_ledger(Name=ledger_name)
39 | result.pop('ResponseMetadata')
40 | logger.info('Success. Ledger description: {}.'.format(result))
41 | return result
42 |
43 |
44 | def main(ledger_name=Constants.LEDGER_NAME):
45 | """
46 | Describe a QLDB ledger.
47 | """
48 | try:
49 | describe_ledger(ledger_name)
50 | except Exception as e:
51 | logger.exception('Unable to describe a ledger.')
52 | raise e
53 |
54 |
55 | if __name__ == '__main__':
56 | main()
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Amazon QLDB Python DMV Sample App
2 | [](https://github.com/aws-samples/amazon-qldb-dmv-sample-python/blob/master/LICENSE)
3 | [](https://aws.amazon.com/qldb/)
4 |
5 | The samples in this project demonstrate several uses of Amazon Quantum Ledger Database (QLDB).
6 |
7 | For our tutorial, see [Python and Amazon QLDB](https://docs.aws.amazon.com/qldb/latest/developerguide/getting-started.python.html).
8 |
9 | ## Requirements
10 |
11 | ### Basic Configuration
12 |
13 | See [Accessing Amazon QLDB](https://docs.aws.amazon.com/qldb/latest/developerguide/accessing.html) for information on connecting to AWS.
14 |
15 | See [Setting Region](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html#configuration) page for more information on using the AWS SDK for Python. You will need to set a region before running the sample code.
16 |
17 | ### Required Python versions
18 |
19 | DMV Sample App v1.x requires Python 3.4 or later.
20 |
21 | DMV Sample App v2.x requires Python 3.7 or later.
22 |
23 | Please see the link below for more detail to install Python:
24 |
25 | * [Python Installation](https://www.python.org/downloads/)
26 |
27 | ## Installing the driver and dependencies
28 |
29 | Install Python QLDB driver and other dependencies using pip:
30 |
31 | ```
32 | pip install -r requirements.txt
33 | ```
34 |
35 | ## Running the Sample code
36 |
37 | The sample code creates a ledger with tables and indexes, and inserts some documents into those tables,
38 | among other things. Each of the examples in this project can be run in the following way:
39 |
40 | ```python
41 | python create_ledger.py
42 | ```
43 |
44 | The above example will build the CreateLedger class with the necessary dependencies and create a ledger named:
45 | `vehicle-registration`. You may run other examples after creating a ledger.
46 |
47 | ## Documentation
48 |
49 | Sphinx is used for documentation. You can generate HTML locally with the following:
50 |
51 | ```
52 | $ pip install -r requirements-docs.txt
53 | $ pip install -e .
54 | $ cd docs
55 | $ make html
56 | ```
57 |
58 | ## License
59 |
60 | This library is licensed under the MIT-0 License.
61 |
--------------------------------------------------------------------------------
/pyqldbsamples/list_tables.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 |
21 | from pyqldbsamples.connect_to_ledger import create_qldb_driver
22 | from pyqldbsamples.constants import Constants
23 |
24 | logger = getLogger(__name__)
25 | basicConfig(level=INFO)
26 |
27 |
28 | def list_tables(ledger_name):
29 | """
30 | List all tables.
31 |
32 | :type ledger_name: str
33 | :param ledger_name: The name of the ledger.
34 |
35 | :rtype: list
36 | :return: List of tables.
37 | """
38 | logger.info("Let's list all the tables...")
39 | with create_qldb_driver(ledger_name) as driver:
40 | logger.info("Success. List of tables:")
41 | tables = driver.list_tables()
42 | for table in tables:
43 | logger.info(table)
44 | return tables
45 |
46 |
47 | def main(ledger_name=Constants.LEDGER_NAME):
48 | """
49 | List all the tables in the configured ledger in QLDB.
50 | """
51 | try:
52 | list_tables(ledger_name)
53 | except Exception as e:
54 | logger.exception('Unable to list tables!')
55 | raise e
56 |
57 |
58 | if __name__ == '__main__':
59 | main()
60 |
--------------------------------------------------------------------------------
/pyqldbsamples/get_digest.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 |
21 | from boto3 import client
22 |
23 | from pyqldbsamples.constants import Constants
24 | from pyqldbsamples.qldb.qldb_string_utils import digest_response_to_string
25 |
26 | logger = getLogger(__name__)
27 | basicConfig(level=INFO)
28 | qldb_client = client('qldb')
29 |
30 |
31 | def get_digest_result(name):
32 | """
33 | Get the digest of a ledger's journal.
34 |
35 | :type name: str
36 | :param name: Name of the ledger to operate on.
37 |
38 | :rtype: dict
39 | :return: The digest in a 256-bit hash value and a block address.
40 | """
41 | logger.info("Let's get the current digest of the ledger named {}".format(name))
42 | result = qldb_client.get_digest(Name=name)
43 | logger.info('Success. LedgerDigest: {}.'.format(digest_response_to_string(result)))
44 | return result
45 |
46 |
47 | def main(ledger_name=Constants.LEDGER_NAME):
48 | """
49 | This is an example for retrieving the digest of a particular ledger.
50 | """
51 | try:
52 | get_digest_result(ledger_name)
53 | except Exception as e:
54 | logger.exception('Unable to get a ledger digest!')
55 | raise e
56 |
57 |
58 | if __name__ == '__main__':
59 | main()
60 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '23 10 * * 4'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'python' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
37 | # Learn more:
38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
39 |
40 | steps:
41 | - name: Checkout repository
42 | uses: actions/checkout@v2
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v1
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v1
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v1
72 |
--------------------------------------------------------------------------------
/pyqldbsamples/scan_table.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 |
21 | from pyqldbsamples.model.sample_data import print_result
22 | from pyqldbsamples.connect_to_ledger import create_qldb_driver
23 | from pyqldbsamples.constants import Constants
24 |
25 | logger = getLogger(__name__)
26 | basicConfig(level=INFO)
27 |
28 |
29 | def scan_table(driver, table_name):
30 | """
31 | Scan for all the documents in a table.
32 |
33 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
34 | :param driver: An instance of the QldbDriver class.
35 |
36 | :type table_name: str
37 | :param table_name: The name of the table to operate on.
38 |
39 | :rtype: :py:class:`pyqldb.cursor.buffered_cursor.BufferedCursor`
40 | :return: Cursor on the result set of a statement query.
41 | """
42 | logger.info('Scanning {}...'.format(table_name))
43 | query = 'SELECT * FROM {}'.format(table_name)
44 | return driver.execute_lambda(lambda executor: executor.execute_statement(query))
45 |
46 |
47 | def main(ledger_name=Constants.LEDGER_NAME):
48 | """
49 | Scan for all the documents in a table.
50 | """
51 | try:
52 | with create_qldb_driver(ledger_name) as driver:
53 | # Scan all the tables and print their documents.
54 | tables = driver.list_tables()
55 | for table in tables:
56 | cursor = scan_table(driver, table)
57 | logger.info('Scan successful!')
58 | print_result(cursor)
59 | except Exception as e:
60 | logger.exception('Unable to scan tables.')
61 | raise e
62 |
63 |
64 | if __name__ == '__main__':
65 | main()
66 |
--------------------------------------------------------------------------------
/pyqldbsamples/create_table.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 |
21 | from pyqldbsamples.constants import Constants
22 | from pyqldbsamples.connect_to_ledger import create_qldb_driver
23 |
24 | logger = getLogger(__name__)
25 | basicConfig(level=INFO)
26 |
27 |
28 | def create_table(driver, table_name):
29 | """
30 | Create a table with the specified name.
31 |
32 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
33 | :param driver: An instance of the QldbDriver class.
34 |
35 | :type table_name: str
36 | :param table_name: Name of the table to create.
37 |
38 | :rtype: int
39 | :return: The number of changes to the database.
40 | """
41 | logger.info("Creating the '{}' table...".format(table_name))
42 | statement = 'CREATE TABLE {}'.format(table_name)
43 | cursor = driver.execute_lambda(lambda executor: executor.execute_statement(statement))
44 | logger.info('{} table created successfully.'.format(table_name))
45 | return len(list(cursor))
46 |
47 |
48 | def main(ledger_name=Constants.LEDGER_NAME):
49 | """
50 | Create registrations, vehicles, owners, and licenses tables.
51 | """
52 | try:
53 | with create_qldb_driver(ledger_name) as driver:
54 | create_table(driver, Constants.DRIVERS_LICENSE_TABLE_NAME)
55 | create_table(driver, Constants.PERSON_TABLE_NAME)
56 | create_table(driver, Constants.VEHICLE_TABLE_NAME)
57 | create_table(driver, Constants.VEHICLE_REGISTRATION_TABLE_NAME)
58 | logger.info('Tables created successfully.')
59 | except Exception as e:
60 | logger.exception('Errors creating tables.')
61 | raise e
62 |
63 |
64 | if __name__ == '__main__':
65 | main()
66 |
--------------------------------------------------------------------------------
/pyqldbsamples/describe_journal_export.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 | from sys import argv
21 |
22 | from boto3 import client
23 |
24 | from pyqldbsamples.constants import Constants
25 |
26 | logger = getLogger(__name__)
27 | basicConfig(level=INFO)
28 | qldb_client = client('qldb')
29 |
30 |
31 | def describe_journal_export(ledger_name, export_id):
32 | """
33 | Describe a journal export.
34 |
35 | :type ledger_name: str
36 | :param ledger_name: The ledger from which the journal is being exported.
37 |
38 | :type export_id: str
39 | :param export_id: The ExportId of the journal.
40 |
41 | :rtype: dict
42 | :return: Result from the request.
43 | """
44 | logger.info("Let's describe a journal export for ledger with name: {}, exportId: {}.".format(ledger_name,
45 | export_id))
46 | export_result = qldb_client.describe_journal_s3_export(Name=ledger_name, ExportId=export_id)
47 | logger.info('Export described. Result = {}.'.format(export_result['ExportDescription']))
48 | return export_result
49 |
50 |
51 | def main(ledger_name=Constants.LEDGER_NAME):
52 | """
53 | Describe a specific journal export with the given ExportId.
54 | """
55 | if len(argv) != 2:
56 | raise ValueError('Missing ExportId argument in DescribeJournalExport')
57 | export_id = argv[1]
58 |
59 | logger.info('Running describe export journal tutorial with ExportId: {}.'.format(export_id))
60 |
61 | try:
62 | describe_journal_export(ledger_name, export_id)
63 | except Exception as e:
64 | logger.exception('Unable to describe an export!')
65 | raise e
66 |
67 |
68 | if __name__ == '__main__':
69 | main()
70 |
--------------------------------------------------------------------------------
/pyqldbsamples/deletion_protection.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 |
21 | from boto3 import client
22 |
23 | from pyqldbsamples.create_ledger import wait_for_active
24 | from pyqldbsamples.delete_ledger import delete_ledger, set_deletion_protection
25 |
26 | logger = getLogger(__name__)
27 | basicConfig(level=INFO)
28 | qldb_client = client('qldb')
29 |
30 | LEDGER_NAME = 'deletion-protection-demo'
31 |
32 |
33 | def create_with_deletion_protection(ledger_name):
34 | """
35 | Create a new ledger with the specified name and with deletion protection enabled.
36 |
37 | :type ledger_name: str
38 | :param ledger_name: Name for the ledger to be created.
39 |
40 | :rtype: dict
41 | :return: Result from the request.
42 | """
43 | logger.info("Let's create the ledger with name: {}...".format(ledger_name))
44 | result = qldb_client.create_ledger(Name=ledger_name, PermissionsMode='ALLOW_ALL')
45 | logger.info('Success. Ledger state: {}'.format(result.get('State')))
46 | return result
47 |
48 |
49 | def main(ledger_name=LEDGER_NAME):
50 | """
51 | Demonstrate the protection of QLDB ledgers against deletion.
52 | """
53 | try:
54 | create_with_deletion_protection(ledger_name)
55 | wait_for_active(ledger_name)
56 | try:
57 | delete_ledger(ledger_name)
58 | except qldb_client.exceptions.ResourcePreconditionNotMetException:
59 | logger.info('Ledger protected against deletions! Turning off deletion protection now.')
60 | set_deletion_protection(ledger_name, False)
61 | delete_ledger(ledger_name)
62 | except Exception as e:
63 | logger.exception('Error while updating or deleting the ledger!')
64 | raise e
65 |
66 |
67 | if __name__ == '__main__':
68 | main()
69 |
--------------------------------------------------------------------------------
/pyqldbsamples/connect_to_ledger.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 |
21 | from botocore.exceptions import ClientError
22 |
23 | from pyqldb.driver.qldb_driver import QldbDriver
24 | from pyqldbsamples.constants import Constants
25 |
26 | logger = getLogger(__name__)
27 | basicConfig(level=INFO)
28 |
29 |
30 | def create_qldb_driver(ledger_name=Constants.LEDGER_NAME, region_name=None, endpoint_url=None, boto3_session=None):
31 | """
32 | Create a QLDB driver for executing transactions.
33 |
34 | :type ledger_name: str
35 | :param ledger_name: The QLDB ledger name.
36 |
37 | :type region_name: str
38 | :param region_name: See [1].
39 |
40 | :type endpoint_url: str
41 | :param endpoint_url: See [1].
42 |
43 | :type boto3_session: :py:class:`boto3.session.Session`
44 | :param boto3_session: The boto3 session to create the client with (see [1]).
45 |
46 | :rtype: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
47 | :return: A QLDB driver object.
48 |
49 | [1]: `Boto3 Session.client Reference `.
50 | """
51 | qldb_driver = QldbDriver(ledger_name=ledger_name, region_name=region_name, endpoint_url=endpoint_url,
52 | boto3_session=boto3_session)
53 | return qldb_driver
54 |
55 |
56 | def main(ledger_name=Constants.LEDGER_NAME):
57 | """
58 | Connect to a given ledger using default settings.
59 | """
60 | try:
61 | with create_qldb_driver(ledger_name) as driver:
62 | logger.info('Listing table names ')
63 | for table in driver.list_tables():
64 | logger.info(table)
65 | except ClientError as ce:
66 | logger.exception('Unable to list tables.')
67 | raise ce
68 |
69 |
70 | if __name__ == '__main__':
71 | main()
72 |
--------------------------------------------------------------------------------
/pyqldbsamples/find_vehicles.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 |
21 | from pyqldbsamples.model.sample_data import get_document_ids, print_result, SampleData
22 | from pyqldbsamples.constants import Constants
23 | from pyqldbsamples.connect_to_ledger import create_qldb_driver
24 |
25 | logger = getLogger(__name__)
26 | basicConfig(level=INFO)
27 |
28 |
29 | def find_vehicles_for_owner(driver, gov_id):
30 | """
31 | Find vehicles registered under a driver using their government ID.
32 |
33 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
34 | :param driver: An instance of the QldbDriver class.
35 |
36 | :type gov_id: str
37 | :param gov_id: The owner's government ID.
38 | """
39 | document_ids = driver.execute_lambda(lambda executor: get_document_ids(executor, Constants.PERSON_TABLE_NAME,
40 | 'GovId', gov_id))
41 |
42 | query = "SELECT Vehicle FROM Vehicle INNER JOIN VehicleRegistration AS r " \
43 | "ON Vehicle.VIN = r.VIN WHERE r.Owners.PrimaryOwner.PersonId = ?"
44 |
45 | for ids in document_ids:
46 | cursor = driver.execute_lambda(lambda executor: executor.execute_statement(query, ids))
47 | logger.info('List of Vehicles for owner with GovId: {}...'.format(gov_id))
48 | print_result(cursor)
49 |
50 |
51 | def main(ledger_name=Constants.LEDGER_NAME):
52 | """
53 | Find all vehicles registered under a person.
54 | """
55 | try:
56 | with create_qldb_driver(ledger_name) as driver:
57 | # Find all vehicles registered under a person.
58 | gov_id = SampleData.PERSON[0]['GovId']
59 | find_vehicles_for_owner(driver, gov_id)
60 | except Exception as e:
61 | logger.exception('Error getting vehicles for owner.')
62 | raise e
63 |
64 |
65 | if __name__ == '__main__':
66 | main()
67 |
--------------------------------------------------------------------------------
/pyqldbsamples/deregister_drivers_license.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 |
21 | from pyqldbsamples.connect_to_ledger import create_qldb_driver
22 | from pyqldbsamples.constants import Constants
23 | from pyqldbsamples.model.sample_data import SampleData, convert_object_to_ion
24 |
25 | logger = getLogger(__name__)
26 | basicConfig(level=INFO)
27 |
28 |
29 | def deregister_drivers_license(driver, license_number):
30 | """
31 | De-register a driver's license with the given license number.
32 |
33 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
34 | :param driver: An instance of the QldbDriver class.
35 |
36 | :type license_number: str
37 | :param license_number: The license number of the driver's license to de-register.
38 | """
39 | logger.info('De-registering license with license number: {}.'.format(license_number))
40 | statement = 'DELETE FROM DriversLicense AS d WHERE d.LicenseNumber = ?'
41 | parameter = convert_object_to_ion(license_number)
42 | cursor = driver.execute_lambda(lambda executor: executor.execute_statement(statement, parameter))
43 |
44 | try:
45 | # Check whether cursor is empty.
46 | next(cursor)
47 | logger.info("Successfully de-registered driver's license: {}.".format(license_number))
48 | except StopIteration:
49 | logger.error('Error de-registering license, license {} not found.'.format(license_number))
50 |
51 |
52 | def main(ledger_name=Constants.LEDGER_NAME):
53 | """
54 | De-register a driver's license.
55 | """
56 | license_number = SampleData.DRIVERS_LICENSE[1]['LicenseNumber']
57 |
58 | try:
59 | with create_qldb_driver(ledger_name) as driver:
60 | deregister_drivers_license(driver, license_number)
61 | except Exception as e:
62 | logger.exception('Error deleting driver license.')
63 | raise e
64 |
65 |
66 | if __name__ == '__main__':
67 | main()
68 |
--------------------------------------------------------------------------------
/pyqldbsamples/qldb/journal_block.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 |
17 |
18 | class JournalBlock:
19 | """
20 | Represents a JournalBlock that was recorded after executing a transaction in the ledger.
21 | """
22 | def __init__(self, block_address, transaction_id, block_timestamp, block_hash, entries_hash, previous_block_hash,
23 | entries_hash_list, transaction_info, redaction_info, revisions):
24 | self.block_address = block_address
25 | self.transaction_id = transaction_id
26 | self.block_timestamp = block_timestamp
27 | self.block_hash = block_hash
28 | self.entries_hash = entries_hash
29 | self.previous_block_hash = previous_block_hash
30 | self.entries_hash_list = entries_hash_list
31 | self.transaction_info = transaction_info
32 | self.redaction_info = redaction_info
33 | self.revisions = revisions
34 |
35 |
36 | def from_ion(ion_value):
37 | """
38 | Construct a new JournalBlock object from an IonStruct.
39 |
40 | :type ion_value: :py:class:`amazon.ion.simple_types.IonSymbol`
41 | :param ion_value: The IonStruct returned by QLDB that represents a journal block.
42 |
43 | :rtype: :py:class:`pyqldbsamples.qldb.journal_block.JournalBlock`
44 | :return: The constructed JournalBlock object.
45 | """
46 | block_address = ion_value.get('blockAddress')
47 | transaction_id = ion_value.get('transactionId')
48 | block_timestamp = ion_value.get('blockTimestamp')
49 | block_hash = ion_value.get('blockHash')
50 | entries_hash = ion_value.get('entriesHash')
51 | previous_block_hash = ion_value.get('previousBlockHash')
52 | entries_hash_list = ion_value.get('entriesHashList')
53 | transaction_info = ion_value.get('transactionInfo')
54 | redaction_info = ion_value.get('redactionInfo')
55 | revisions = ion_value.get('revisions')
56 |
57 | journal_block = JournalBlock(block_address, transaction_id, block_timestamp, block_hash, entries_hash,
58 | previous_block_hash, entries_hash_list, transaction_info, redaction_info, revisions)
59 | return journal_block
60 |
--------------------------------------------------------------------------------
/pyqldbsamples/create_ledger.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 | from time import sleep
21 |
22 | from boto3 import client
23 |
24 | from pyqldbsamples.constants import Constants
25 |
26 | logger = getLogger(__name__)
27 | basicConfig(level=INFO)
28 | qldb_client = client('qldb')
29 |
30 | LEDGER_CREATION_POLL_PERIOD_SEC = 10
31 | ACTIVE_STATE = "ACTIVE"
32 |
33 |
34 | def create_ledger(name):
35 | """
36 | Create a new ledger with the specified name.
37 |
38 | :type name: str
39 | :param name: Name for the ledger to be created.
40 |
41 | :rtype: dict
42 | :return: Result from the request.
43 | """
44 | logger.info("Let's create the ledger named: {}...".format(name))
45 | result = qldb_client.create_ledger(Name=name, PermissionsMode='ALLOW_ALL')
46 | logger.info('Success. Ledger state: {}.'.format(result.get('State')))
47 | return result
48 |
49 |
50 | def wait_for_active(name):
51 | """
52 | Wait for the newly created ledger to become active.
53 |
54 | :type name: str
55 | :param name: The ledger to check on.
56 |
57 | :rtype: dict
58 | :return: Result from the request.
59 | """
60 | logger.info('Waiting for ledger to become active...')
61 | while True:
62 | result = qldb_client.describe_ledger(Name=name)
63 | if result.get('State') == ACTIVE_STATE:
64 | logger.info('Success. Ledger is active and ready to use.')
65 | return result
66 | logger.info('The ledger is still creating. Please wait...')
67 | sleep(LEDGER_CREATION_POLL_PERIOD_SEC)
68 |
69 |
70 | def main(ledger_name=Constants.LEDGER_NAME):
71 | """
72 | Create a ledger and wait for it to be active.
73 | """
74 | try:
75 | create_ledger(ledger_name)
76 | wait_for_active(ledger_name)
77 | except Exception as e:
78 | logger.exception('Unable to create the ledger!')
79 | raise e
80 |
81 |
82 | if __name__ == '__main__':
83 | main()
84 |
--------------------------------------------------------------------------------
/pyqldbsamples/qldb/qldb_string_utils.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | from amazon.ion.simpleion import dumps, loads
17 |
18 |
19 | def value_holder_to_string(value_holder):
20 | """
21 | Returns the string representation of a given `value_holder`.
22 |
23 | :type value_holder: dict
24 | :param value_holder: The `value_holder` to convert to string.
25 |
26 | :rtype: str
27 | :return: The string representation of the supplied `value_holder`.
28 | """
29 | ret_val = dumps(loads(value_holder), binary=False, indent=' ', omit_version_marker=True)
30 | val = '{{ IonText: {}}}'.format(ret_val)
31 | return val
32 |
33 |
34 | def block_response_to_string(block_response):
35 | """
36 | Returns the string representation of a given `block_response`.
37 |
38 | :type block_response: dict
39 | :param block_response: The `block_response` to convert to string.
40 |
41 | :rtype: str
42 | :return: The string representation of the supplied `block_response`.
43 | """
44 | string = ''
45 | if block_response.get('Block', {}).get('IonText') is not None:
46 | string += 'Block: ' + value_holder_to_string(block_response['Block']['IonText']) + ', '
47 |
48 | if block_response.get('Proof', {}).get('IonText') is not None:
49 | string += 'Proof: ' + value_holder_to_string(block_response['Proof']['IonText'])
50 |
51 | return '{' + string + '}'
52 |
53 |
54 | def digest_response_to_string(digest_response):
55 | """
56 | Returns the string representation of a given `digest_response`.
57 |
58 | :type digest_response: dict
59 | :param digest_response: The `digest_response` to convert to string.
60 |
61 | :rtype: str
62 | :return: The string representation of the supplied `digest_response`.
63 | """
64 | string = ''
65 | if digest_response.get('Digest') is not None:
66 | string += 'Digest: ' + str(digest_response['Digest']) + ', '
67 |
68 | if digest_response.get('DigestTipAddress', {}).get('IonText') is not None:
69 | string += 'DigestTipAddress: ' + value_holder_to_string(digest_response['DigestTipAddress']['IonText'])
70 |
71 | return '{' + string + '}'
72 |
--------------------------------------------------------------------------------
/pyqldbsamples/create_index.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 |
21 | from pyqldbsamples.constants import Constants
22 | from pyqldbsamples.connect_to_ledger import create_qldb_driver
23 |
24 | logger = getLogger(__name__)
25 | basicConfig(level=INFO)
26 |
27 |
28 | def create_index(driver, table_name, index_attribute):
29 | """
30 | Create an index for a particular table.
31 |
32 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
33 | :param driver: An instance of the QldbDriver class.
34 |
35 | :type table_name: str
36 | :param table_name: Name of the table to add indexes for.
37 |
38 | :type index_attribute: str
39 | :param index_attribute: Index to create on a single attribute.
40 |
41 | :rtype: int
42 | :return: The number of changes to the database.
43 | """
44 | logger.info("Creating index on '{}'...".format(index_attribute))
45 | statement = 'CREATE INDEX on {} ({})'.format(table_name, index_attribute)
46 | cursor = driver.execute_lambda(lambda executor: executor.execute_statement(statement))
47 | return len(list(cursor))
48 |
49 |
50 | def main(ledger_name=Constants.LEDGER_NAME):
51 | """
52 | Create indexes on tables in a particular ledger.
53 | """
54 | logger.info('Creating indexes on all tables...')
55 | try:
56 | with create_qldb_driver(ledger_name) as driver:
57 | create_index(driver, Constants.PERSON_TABLE_NAME, Constants.GOV_ID_INDEX_NAME)
58 | create_index(driver, Constants.VEHICLE_TABLE_NAME, Constants.VEHICLE_VIN_INDEX_NAME)
59 | create_index(driver, Constants.VEHICLE_REGISTRATION_TABLE_NAME, Constants.LICENSE_PLATE_NUMBER_INDEX_NAME)
60 | create_index(driver, Constants.VEHICLE_REGISTRATION_TABLE_NAME, Constants.VEHICLE_VIN_INDEX_NAME)
61 | create_index(driver, Constants.DRIVERS_LICENSE_TABLE_NAME, Constants.PERSON_ID_INDEX_NAME)
62 | create_index(driver, Constants.DRIVERS_LICENSE_TABLE_NAME, Constants.LICENSE_NUMBER_INDEX_NAME)
63 | logger.info('Indexes created successfully.')
64 | except Exception as e:
65 | logger.exception('Unable to create indexes.')
66 | raise e
67 |
68 |
69 | if __name__ == '__main__':
70 | main()
71 |
--------------------------------------------------------------------------------
/pyqldbsamples/list_journal_exports.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 |
21 | from boto3 import client
22 |
23 | from pyqldbsamples.constants import Constants
24 |
25 | logger = getLogger(__name__)
26 | basicConfig(level=INFO)
27 | MAX_RESULTS = 2
28 | qldb_client = client('qldb')
29 |
30 |
31 | def list_all_journal_exports():
32 | """
33 | List all journal exports.
34 | """
35 | logger.info("Let's list journal exports for the AWS account.")
36 |
37 | next_token = ''
38 | list_of_results = []
39 | while next_token is not None:
40 | if next_token == '':
41 | result = qldb_client.list_journal_s3_exports(MaxResults=MAX_RESULTS)
42 | else:
43 | result = qldb_client.list_journal_s3_exports(MaxResults=MAX_RESULTS, NextToken=next_token)
44 | next_token = result.get('NextToken')
45 | if result.get('JournalS3Exports') != []:
46 | list_of_results.append(result.get('JournalS3Exports'))
47 | logger.info('Success. List of journal exports: {}.'.format(list_of_results))
48 |
49 |
50 | def list_journal_export_with_ledger_name(ledger_name):
51 | """
52 | List all journal exports for the given ledger.
53 |
54 | :type ledger_name: str
55 | :param ledger_name: Name of the ledger to list journal exports for.
56 | """
57 | logger.info("Let's list journal exports for the ledger with name: {}...".format(ledger_name))
58 |
59 | next_token = ''
60 | list_of_results = []
61 | while next_token is not None:
62 | if next_token == '':
63 | result = qldb_client.list_journal_s3_exports_for_ledger(Name=ledger_name, MaxResults=MAX_RESULTS)
64 | else:
65 | result = qldb_client.list_journal_s3_exports_for_ledger(Name=ledger_name, MaxResults=MAX_RESULTS,
66 | NextToken=next_token)
67 | next_token = result.get('NextToken')
68 | if result.get('JournalS3Exports') != []:
69 | list_of_results.append(result.get('JournalS3Exports'))
70 | logger.info('Success. List of journal exports: {}.'.format(list_of_results))
71 |
72 |
73 | def main(ledger_name=Constants.LEDGER_NAME):
74 | """
75 | List the journal exports of a given QLDB ledger.
76 | """
77 | try:
78 | list_journal_export_with_ledger_name(ledger_name)
79 | except Exception as e:
80 | logger.exception('Unable to list exports!')
81 | raise e
82 |
83 |
84 | if __name__ == '__main__':
85 | main()
86 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build CI
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | env:
10 | # This value needs to match the build matrix configuration job total.
11 | job-total: 12
12 |
13 | permissions:
14 | id-token: write
15 | contents: read
16 |
17 | jobs:
18 | generate-unique-id:
19 | runs-on: ubuntu-latest
20 | steps:
21 | - name: Generate unique id
22 | id: unique_id
23 | run: echo "::set-output name=id::$RANDOM"
24 | outputs:
25 | id: ${{ steps.unique_id.outputs.id }}
26 |
27 | build:
28 | needs: [generate-unique-id]
29 | runs-on: ${{ matrix.os }}
30 | strategy:
31 | max-parallel: 6
32 | matrix :
33 | os: [ubuntu-latest, windows-2019, macos-latest]
34 | python-version: ['3.7', '3.8', '3.9', '3.10']
35 | steps:
36 | - name: Configure AWS Credentials
37 | uses: aws-actions/configure-aws-credentials@v1
38 | with:
39 | aws-region: us-east-2
40 | role-to-assume: arn:aws:iam::264319671630:role/GitHubSamplesActionsOidc
41 |
42 | - uses: actions/checkout@v2
43 | - name: Set up Python ${{ matrix.python-version }}
44 | uses: actions/setup-python@v2
45 | with:
46 | python-version: ${{ matrix.python-version }}
47 | - name: Install dependencies
48 | run: |
49 | ls
50 | pip install -r requirements.txt
51 | pip install -e .
52 | - name: Lint with flake8
53 | run: |
54 | pip install flake8
55 | # stop the build if there are Python syntax errors or undefined names
56 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
57 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
58 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
59 | - name: Test with pytest
60 | id: test
61 | run: |
62 | echo "::set-output name=job-total::${{ strategy.job-total }}"
63 | pip install pytest
64 | pytest --ledger_suffix ${{ needs.generate-unique-id.outputs.id}}${{ strategy.job-index }}
65 | outputs:
66 | job-total: ${{ steps.test.outputs.job-total }}
67 |
68 | cleanup:
69 | # This job will always run
70 | if: ${{ always() && needs.generate-unique-id.outputs.id != '' }}
71 | runs-on: ubuntu-latest
72 | needs: [generate-unique-id, build]
73 |
74 | steps:
75 | - name: Configure AWS Credentials
76 | uses: aws-actions/configure-aws-credentials@v1
77 | with:
78 | aws-region: us-east-2
79 | role-to-assume: arn:aws:iam::264319671630:role/GitHubSamplesActionsOidc
80 | - uses: actions/checkout@v2
81 | - name: Set up Python ${{ matrix.python-version }}
82 | uses: actions/setup-python@v2
83 | with:
84 | python-version: 3.8
85 | - name: Install dependencies
86 | run: |
87 | ls
88 | pip install -r requirements.txt
89 | pip install -e .
90 | - name: Delete QLDB resources
91 | run: |
92 | cd tests
93 |
94 | # If the build job was cancelled by user, we will need to use the job-total global environment variable.
95 | if [ -z "${{ needs.build.outputs.job-total }}" ]
96 | then
97 | let total=${{env.job-total}}
98 | else
99 | let total=${{needs.build.outputs.job-total }}
100 | fi
101 |
102 | for ((i=0;i max_poll_time:
27 | break
28 | sleep(3)
29 |
30 |
31 | def force_delete_ledger(ledger_name):
32 | try:
33 | set_deletion_protection(ledger_name, False)
34 | delete_ledger(ledger_name)
35 | wait_for_deleted(ledger_name)
36 | except Exception:
37 | pass
38 |
39 |
40 | def force_delete_s3_bucket(bucket_name):
41 | s3_resource = resource('s3')
42 | bucket = s3_resource.Bucket(bucket_name)
43 |
44 | try:
45 | for key in bucket.objects.all():
46 | key.delete()
47 | bucket.delete()
48 | except Exception:
49 | pass
50 |
51 |
52 | def force_delete_role(role_name):
53 | iam = resource('iam')
54 | role = iam.Role(role_name)
55 |
56 | try:
57 | role.delete()
58 | except Exception:
59 | pass
60 |
61 |
62 | def force_delete_role_policies(role_name):
63 | iam_client = resource('iam')
64 | role = iam_client.Role(role_name)
65 |
66 | try:
67 | list_of_polices = list(role.attached_policies.all())
68 | for policy in list_of_polices:
69 | policy.detach_role(RoleName=role_name)
70 |
71 | policy = iam_client.Policy(policy.arn)
72 | policy.delete()
73 | except Exception:
74 | pass
75 |
76 |
77 | def get_deletion_ledger_name(ledger_suffix):
78 | return 'py-dmv-' + ledger_suffix + '-delete'
79 |
80 |
81 | def get_ledger_name(ledger_suffix):
82 | return 'py-dmv-' + ledger_suffix
83 |
84 |
85 | def get_role_name(ledger_suffix):
86 | return 'github-actions-' + ledger_suffix + '-role'
87 |
88 |
89 | def get_role_policy_name(ledger_suffix):
90 | return 'github-actions-' + ledger_suffix + '-policy'
91 |
92 |
93 | def get_s3_bucket_name(ledger_suffix):
94 | return 'github-actions-' + ledger_suffix + '-bucket'
95 |
96 |
97 | def get_tag_ledger_name(ledger_suffix):
98 | return 'py-dmv-' + ledger_suffix + '-tags'
99 |
100 |
101 | def delete_resources(ledger_suffix):
102 | role_name = get_role_name(ledger_suffix)
103 | s3_bucket_name = get_s3_bucket_name(ledger_suffix)
104 | ledger_name = get_ledger_name(ledger_suffix)
105 | deletion_ledger_name = get_deletion_ledger_name(ledger_suffix)
106 | tag_ledger_name = get_tag_ledger_name(ledger_suffix)
107 |
108 | force_delete_role_policies(role_name)
109 | force_delete_role(role_name)
110 | force_delete_s3_bucket(s3_bucket_name)
111 |
112 | force_delete_ledger(ledger_name)
113 | force_delete_ledger(deletion_ledger_name)
114 | force_delete_ledger(tag_ledger_name)
115 |
116 |
117 | if __name__ == '__main__':
118 | if len(argv) > 1:
119 | ledger_suffix = argv[1]
120 | delete_resources(ledger_suffix)
121 |
--------------------------------------------------------------------------------
/pyqldbsamples/delete_ledger.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 | from time import sleep
21 |
22 | from boto3 import client
23 |
24 | from pyqldbsamples.constants import Constants
25 | from pyqldbsamples.describe_ledger import describe_ledger
26 |
27 | logger = getLogger(__name__)
28 | basicConfig(level=INFO)
29 | qldb_client = client('qldb')
30 |
31 | LEDGER_DELETION_POLL_PERIOD_SEC = 20
32 |
33 |
34 | def delete_ledger(ledger_name):
35 | """
36 | Send a request to QLDB to delete the specified ledger.
37 |
38 | :type ledger_name: str
39 | :param ledger_name: Name for the ledger to be deleted.
40 |
41 | :rtype: dict
42 | :return: Result from the request.
43 | """
44 | logger.info('Attempting to delete the ledger with name: {}...'.format(ledger_name))
45 | result = qldb_client.delete_ledger(Name=ledger_name)
46 | logger.info('Success.')
47 | return result
48 |
49 |
50 | def wait_for_deleted(ledger_name):
51 | """
52 | Wait for the ledger to be deleted.
53 |
54 | :type ledger_name: str
55 | :param ledger_name: The ledger to check on.
56 | """
57 | logger.info('Waiting for the ledger to be deleted...')
58 | while True:
59 | try:
60 | describe_ledger(ledger_name)
61 | logger.info('The ledger is still being deleted. Please wait...')
62 | sleep(LEDGER_DELETION_POLL_PERIOD_SEC)
63 | except qldb_client.exceptions.ResourceNotFoundException:
64 | logger.info('Success. The ledger is deleted.')
65 | break
66 |
67 |
68 | def set_deletion_protection(ledger_name, deletion_protection):
69 | """
70 | Update an existing ledger's deletion protection.
71 |
72 | :type ledger_name: str
73 | :param ledger_name: Name of the ledger to update.
74 |
75 | :type deletion_protection: bool
76 | :param deletion_protection: Enable or disable the deletion protection.
77 |
78 | :rtype: dict
79 | :return: Result from the request.
80 | """
81 | logger.info("Let's set deletion protection to {} for the ledger with name {}.".format(deletion_protection,
82 | ledger_name))
83 | result = qldb_client.update_ledger(Name=ledger_name, DeletionProtection=deletion_protection)
84 | logger.info('Success. Ledger updated: {}'.format(result))
85 |
86 |
87 | def main(ledger_name=Constants.LEDGER_NAME):
88 | """
89 | Delete a ledger.
90 | """
91 | try:
92 | set_deletion_protection(ledger_name, False)
93 | delete_ledger(ledger_name)
94 | wait_for_deleted(ledger_name)
95 | except Exception as e:
96 | logger.exception('Unable to delete the ledger.')
97 | raise e
98 |
99 |
100 | if __name__ == '__main__':
101 | main()
102 |
--------------------------------------------------------------------------------
/pyqldbsamples/query_history.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from datetime import datetime, timedelta
20 | from logging import basicConfig, getLogger, INFO
21 |
22 | from pyqldbsamples.model.sample_data import print_result, get_document_ids, SampleData
23 | from pyqldbsamples.constants import Constants
24 | from pyqldbsamples.connect_to_ledger import create_qldb_driver
25 |
26 | logger = getLogger(__name__)
27 | basicConfig(level=INFO)
28 |
29 |
30 | def format_date_time(date_time):
31 | """
32 | Format the given date time to a string.
33 |
34 | :type date_time: :py:class:`datetime.datetime`
35 | :param date_time: The date time to format.
36 |
37 | :rtype: str
38 | :return: The formatted date time.
39 | """
40 | return date_time.strftime('`%Y-%m-%dT%H:%M:%S.%fZ`')
41 |
42 |
43 | def previous_primary_owners(driver, vin):
44 | """
45 | Find previous primary owners for the given VIN in a single transaction.
46 | In this example, query the `VehicleRegistration` history table to find all previous primary owners for a VIN.
47 |
48 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
49 | :param driver: An instance of the QldbDriver class.
50 |
51 | :type vin: str
52 | :param vin: VIN to find previous primary owners for.
53 | """
54 | person_ids = driver.execute_lambda(lambda executor: get_document_ids(executor,
55 | Constants.VEHICLE_REGISTRATION_TABLE_NAME,
56 | 'VIN', vin))
57 |
58 | todays_date = datetime.utcnow() - timedelta(seconds=1)
59 | three_months_ago = todays_date - timedelta(days=90)
60 | query = 'SELECT data.Owners.PrimaryOwner, metadata.version FROM history({}, {}, {}) AS h WHERE h.metadata.id = ?'.\
61 | format(Constants.VEHICLE_REGISTRATION_TABLE_NAME, format_date_time(three_months_ago),
62 | format_date_time(todays_date))
63 |
64 | for ids in person_ids:
65 | logger.info("Querying the 'VehicleRegistration' table's history using VIN: {}.".format(vin))
66 | cursor = driver.execute_lambda(lambda executor: executor.execute_statement(query, ids))
67 | if not (print_result(cursor)) > 0:
68 | logger.info('No modification history found within the given time frame for document ID: {}'.format(ids))
69 |
70 |
71 | def main(ledger_name=Constants.LEDGER_NAME):
72 | """
73 | Query a table's history for a particular set of documents.
74 | """
75 | try:
76 | with create_qldb_driver(ledger_name) as driver:
77 | vin = SampleData.VEHICLE_REGISTRATION[0]['VIN']
78 | previous_primary_owners(driver, vin)
79 | logger.info('Successfully queried history.')
80 | except Exception as e:
81 | logger.exception('Unable to query history to find previous owners.')
82 | raise e
83 |
84 |
85 | if __name__ == '__main__':
86 | main()
87 |
--------------------------------------------------------------------------------
/pyqldbsamples/tag_resource.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 |
21 | from boto3 import client
22 |
23 | from pyqldbsamples.constants import Constants
24 | from pyqldbsamples.create_ledger import wait_for_active
25 |
26 | logger = getLogger(__name__)
27 | basicConfig(level=INFO)
28 | qldb_client = client('qldb')
29 |
30 | ADD_TAGS = {'Domain': 'Prod'}
31 | CREATE_TAGS = {'IsTest': 'true', 'Domain': 'Test'}
32 | REMOVE_TAGS = ['IsTest']
33 |
34 |
35 | def create_with_tags(name, tags):
36 | """
37 | Create a ledger with the specified name and the given tags.
38 |
39 | :type name: str
40 | :param name: Name of the ledger to be created.
41 |
42 | :type tags: dict
43 | :param tags: A dictionary of tags to create the ledger with.
44 |
45 | :rtype: dict
46 | :return: The result returned by the database.
47 | """
48 | logger.info("Let's create the ledger with name: {}...".format(name))
49 | result = qldb_client.create_ledger(Name=name, Tags=tags, PermissionsMode="ALLOW_ALL")
50 | logger.info('Success. Ledger state: {}.'.format(result.get('State')))
51 | return result
52 |
53 |
54 | def tag_resource(resource_arn, tags):
55 | """
56 | Add one or more tags to the specified QLDB resource.
57 |
58 | :type resource_arn: dict
59 | :param resource_arn: The Amazon Resource Name (ARN) of the ledger to which to add tags.
60 |
61 | :type tags: dict
62 | :param tags: The key-value pairs to add as tags.
63 | """
64 | logger.info("Let's add tags {} for resource with arn: {}...".format(tags, resource_arn))
65 | qldb_client.tag_resource(ResourceArn=resource_arn, Tags=tags)
66 | logger.info('Successfully added tags.')
67 |
68 |
69 | def untag_resource(resource_arn, tag_keys):
70 | """
71 | Remove one or more tags from the specified QLDB resource.
72 |
73 | :type resource_arn: dict
74 | :param resource_arn: The Amazon Resource Name (ARN) of the ledger from which to remove the tags.
75 |
76 | :type tag_keys: list
77 | :param tag_keys: The list of tag keys to remove.
78 | """
79 | logger.info("Let's remove tags {} for resource with arn: {}.".format(tag_keys, resource_arn))
80 | qldb_client.untag_resource(ResourceArn=resource_arn, TagKeys=tag_keys)
81 | logger.info('Successfully removed tags.')
82 |
83 |
84 | def list_tags(resource_arn):
85 | """
86 | Returns all tags for a specified Amazon QLDB resource.
87 |
88 | :type resource_arn: dict
89 | :param resource_arn: The Amazon Resource Name (ARN) for which to list tags off.
90 |
91 | :rtype: dict
92 | :return: All tags on the specified resource.
93 | """
94 | logger.info("Let's list the tags for resource with arn: {}.".format(resource_arn))
95 | result = qldb_client.list_tags_for_resource(ResourceArn=resource_arn)
96 | logger.info('Success. Tags: {}.'.format(result.get('Tags')))
97 | return result
98 |
99 |
100 | def main(ledger_name=Constants.LEDGER_NAME_WITH_TAGS):
101 | """
102 | Tagging and un-tagging resources, including tag on create.
103 | """
104 | try:
105 | result = create_with_tags(ledger_name, CREATE_TAGS)
106 | wait_for_active(ledger_name)
107 | ARN = result.get('Arn')
108 | list_tags(ARN)
109 | untag_resource(ARN, REMOVE_TAGS)
110 | list_tags(ARN)
111 | tag_resource(ARN, ADD_TAGS)
112 | list_tags(ARN)
113 | except Exception as e:
114 | logger.exception('Unable to tag or untag resources!')
115 | raise e
116 |
117 |
118 | if __name__ == '__main__':
119 | main()
120 |
--------------------------------------------------------------------------------
/pyqldbsamples/insert_document.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 |
21 | from pyqldbsamples.constants import Constants
22 | from pyqldbsamples.model.sample_data import convert_object_to_ion, SampleData, get_document_ids_from_dml_results
23 | from pyqldbsamples.connect_to_ledger import create_qldb_driver
24 |
25 | logger = getLogger(__name__)
26 | basicConfig(level=INFO)
27 |
28 |
29 | def update_person_id(document_ids):
30 | """
31 | Update the PersonId value for DriversLicense records and the PrimaryOwner value for VehicleRegistration records.
32 |
33 | :type document_ids: list
34 | :param document_ids: List of document IDs.
35 |
36 | :rtype: list
37 | :return: Lists of updated DriversLicense records and updated VehicleRegistration records.
38 | """
39 | new_drivers_licenses = SampleData.DRIVERS_LICENSE.copy()
40 | new_vehicle_registrations = SampleData.VEHICLE_REGISTRATION.copy()
41 | for i in range(len(SampleData.PERSON)):
42 | drivers_license = new_drivers_licenses[i]
43 | registration = new_vehicle_registrations[i]
44 | drivers_license.update({'PersonId': str(document_ids[i])})
45 | registration['Owners']['PrimaryOwner'].update({'PersonId': str(document_ids[i])})
46 | return new_drivers_licenses, new_vehicle_registrations
47 |
48 |
49 | def insert_documents(driver, table_name, documents):
50 | """
51 | Insert the given list of documents into a table in a single transaction.
52 |
53 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
54 | :param driver: An instance of the QldbDriver class.
55 |
56 | :type table_name: str
57 | :param table_name: Name of the table to insert documents into.
58 |
59 | :type documents: list
60 | :param documents: List of documents to insert.
61 |
62 | :rtype: list
63 | :return: List of documents IDs for the newly inserted documents.
64 | """
65 | logger.info('Inserting some documents in the {} table...'.format(table_name))
66 | statement = 'INSERT INTO {} ?'.format(table_name)
67 | cursor = driver.execute_lambda(lambda executor: executor.execute_statement(statement,
68 | convert_object_to_ion(documents)))
69 | list_of_document_ids = get_document_ids_from_dml_results(cursor)
70 |
71 | return list_of_document_ids
72 |
73 |
74 | def update_and_insert_documents(driver):
75 | """
76 | Handle the insertion of documents and updating PersonIds.
77 |
78 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
79 | :param driver: An instance of the QldbDriver class.
80 | """
81 | list_ids = insert_documents(driver, Constants.PERSON_TABLE_NAME, SampleData.PERSON)
82 |
83 | logger.info("Updating PersonIds for 'DriversLicense' and PrimaryOwner for 'VehicleRegistration'...")
84 | new_licenses, new_registrations = update_person_id(list_ids)
85 |
86 | insert_documents(driver, Constants.VEHICLE_TABLE_NAME, SampleData.VEHICLE)
87 | insert_documents(driver, Constants.VEHICLE_REGISTRATION_TABLE_NAME, new_registrations)
88 | insert_documents(driver, Constants.DRIVERS_LICENSE_TABLE_NAME, new_licenses)
89 |
90 |
91 | def main(ledger_name=Constants.LEDGER_NAME):
92 | """
93 | Insert documents into a table in a QLDB ledger.
94 | """
95 | try:
96 | with create_qldb_driver(ledger_name) as driver:
97 | # An INSERT statement creates the initial revision of a document with a version number of zero.
98 | # QLDB also assigns a unique document identifier in GUID format as part of the metadata.
99 | update_and_insert_documents(driver)
100 | logger.info('Documents inserted successfully!')
101 | except Exception as e:
102 | logger.exception('Error inserting or updating documents.')
103 | raise e
104 |
105 |
106 | if __name__ == '__main__':
107 | main()
108 |
--------------------------------------------------------------------------------
/pyqldbsamples/validate_qldb_hash_chain.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from functools import reduce
20 | from logging import basicConfig, getLogger, INFO
21 | from sys import argv
22 | import time
23 |
24 | from boto3 import client, resource
25 |
26 | from pyqldbsamples.constants import Constants
27 | from pyqldbsamples.describe_journal_export import describe_journal_export
28 | from pyqldbsamples.export_journal import create_export_and_wait_for_completion, create_s3_bucket_if_not_exists, \
29 | set_up_s3_encryption_configuration
30 | from pyqldbsamples.journal_s3_export_reader import read_export
31 | from pyqldbsamples.verifier import join_hash_pairwise
32 |
33 | logger = getLogger(__name__)
34 | basicConfig(level=INFO)
35 |
36 |
37 | def create_journal_export():
38 | """
39 | Export journal contents to a S3 bucket.
40 |
41 | :rtype: str
42 | :return: The ExportId fo the journal export.
43 | """
44 | s3_resource = resource('s3')
45 | sts = client('sts')
46 |
47 | current_time = int(time.time())
48 | identity = sts.get_caller_identity()
49 | bucket_name = '{}-{}'.format(Constants.JOURNAL_EXPORT_S3_BUCKET_NAME_PREFIX, identity['Account'])
50 |
51 | prefix = '{}-{}/'.format(Constants.LEDGER_NAME, current_time)
52 | create_s3_bucket_if_not_exists(bucket_name, s3_resource)
53 |
54 | export_journal_to_s3_result = create_export_and_wait_for_completion(Constants.LEDGER_NAME, bucket_name, prefix,
55 | set_up_s3_encryption_configuration())
56 |
57 | return export_journal_to_s3_result.get('ExportId')
58 |
59 |
60 | def compare_journal_blocks(previous_journal_block, journal_block):
61 | """
62 | Compare the hash values on the given journal blocks.
63 |
64 | :type previous_journal_block: :py:class:`pyqldbsamples.qldb.journal_block.JournalBlock`
65 | :param previous_journal_block: Previous journal block in the chain.
66 |
67 | :type journal_block: :py:class:`pyqldbsamples.qldb.journal_block.JournalBlock`
68 | :param journal_block: Current journal block in the chain.
69 |
70 | :rtype: :py:class:`pyqldbsamples.qldb.journal_block.JournalBlock`
71 | :return: The current journal block in the chain.
72 |
73 | :raises RuntimeError: If the chain hash on the journal block is broken.
74 | """
75 | if previous_journal_block is None:
76 | return journal_block
77 | if previous_journal_block.block_hash != journal_block.previous_block_hash:
78 | raise RuntimeError('Previous block hash does not match!')
79 |
80 | block_hash = join_hash_pairwise(journal_block.entries_hash, previous_journal_block.block_hash)
81 | if block_hash != journal_block.block_hash:
82 | raise RuntimeError("Block hash doesn't match expected block hash. Verification failed.")
83 |
84 | return journal_block
85 |
86 |
87 | def verify(journal_blocks):
88 | """
89 | Validate that the chain hash on the journal block is valid.
90 |
91 | :type journal_blocks: list
92 | :param journal_blocks: A list of journal blocks.
93 |
94 | :return: None if the given list of journal blocks is empty.
95 | """
96 | if len(journal_blocks) == 0:
97 | return
98 | reduce(compare_journal_blocks, journal_blocks)
99 |
100 |
101 | def main(ledger_name=Constants.LEDGER_NAME):
102 | """
103 | Validate the hash chain of a QLDB ledger by stepping through its S3 export.
104 |
105 | This code accepts an exportID as an argument, if exportID is passed the code
106 | will use that or request QLDB to generate a new export to perform QLDB hash
107 | chain validation.
108 | """
109 | s3_client = client('s3')
110 | try:
111 | if len(argv) == 2:
112 | export_id = argv[1]
113 | logger.info('Validating qldb hash chain for ExportId: {}.'.format(export_id))
114 |
115 | else:
116 | logger.info('Requesting qldb to create an export.')
117 | export_id = create_journal_export()
118 |
119 | journal_export = describe_journal_export(ledger_name, export_id).get('ExportDescription')
120 | journal_blocks = read_export(journal_export, s3_client)
121 | verify(journal_blocks)
122 | except Exception as e:
123 | logger.exception('Unable to perform hash chain verification.')
124 | raise e
125 |
126 |
127 | if __name__ == '__main__':
128 | main()
129 |
--------------------------------------------------------------------------------
/pyqldbsamples/renew_drivers_license.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 | from datetime import datetime
21 |
22 | from pyqldbsamples.model.sample_data import SampleData, convert_object_to_ion, get_document_ids_from_dml_results
23 | from pyqldbsamples.connect_to_ledger import create_qldb_driver
24 | from pyqldbsamples.constants import Constants
25 |
26 | logger = getLogger(__name__)
27 | basicConfig(level=INFO)
28 |
29 | VALID_FROM_DATE = datetime(2019, 4, 19)
30 | VALID_TO_DATE = datetime(2023, 4, 19)
31 |
32 |
33 | def verify_driver_from_license_number(driver, license_number):
34 | """
35 | Verify whether a driver exists in the system with the provided license number.
36 |
37 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
38 | :param driver: An instance of the QldbDriver class.
39 |
40 | :type license_number: str
41 | :param license_number: The driver's license number.
42 |
43 | :raises RuntimeError: If driver does not exist in the system.
44 | """
45 | logger.info("Finding person ID with license number: {}.".format(license_number))
46 | query = 'SELECT PersonId FROM DriversLicense AS d WHERE d.LicenseNumber = ?'
47 | person_id = driver.execute_lambda(lambda executor: executor.execute_statement(query,
48 | convert_object_to_ion(license_number)))
49 | pid = next(person_id).get('PersonId')
50 | query = 'SELECT p.* FROM Person AS p BY pid WHERE pid = ?'
51 | cursor = driver.execute_lambda(lambda executor: executor.execute_statement(query, pid))
52 | try:
53 | next(cursor)
54 | except StopIteration:
55 | raise RuntimeError('Unable to find person with ID: {}'.format(pid))
56 |
57 |
58 | def renew_drivers_license(driver, valid_from, valid_to, license_number):
59 | """
60 | Renew the ValidFromDate and ValidToDate of a driver's license.
61 |
62 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
63 | :param driver: An instance of the QldbDriver class.
64 |
65 | :type valid_from: :py:class:`datetime.datetime`
66 | :param valid_from: The new valid-from date.
67 |
68 | :type valid_to: :py:class:`datetime.datetime`
69 | :param valid_to: The new valid-to date.
70 |
71 | :type license_number: str
72 | :param license_number: The license number for the driver's license to renew.
73 |
74 | :raises RuntimeError: If no driver's license was updated.
75 | """
76 | logger.info('Renewing license with license number: {}...'.format(license_number))
77 | update_valid_date = 'UPDATE DriversLicense AS d SET d.ValidFromDate = ?, d.ValidToDate = ? WHERE d.LicenseNumber ' \
78 | '= ?'
79 | cursor = driver.execute_lambda(lambda executor: executor.execute_statement(update_valid_date,
80 | convert_object_to_ion(valid_from),
81 | convert_object_to_ion(valid_to),
82 | convert_object_to_ion(license_number)))
83 | logger.info('DriversLicense Document IDs which had licenses renewed: ')
84 | list_of_licenses = get_document_ids_from_dml_results(cursor)
85 | for license in list_of_licenses:
86 | logger.info(license)
87 | return list_of_licenses
88 |
89 |
90 | def verify_and_renew_license(driver, license_num, valid_from_date, valid_to_date):
91 | """
92 | Verify if the driver of the given license and update the license with the given dates.
93 |
94 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
95 | :param driver: An instance of the QldbDriver class.
96 |
97 | :type license_num: str
98 | :param license_num: The license to verify and renew.
99 |
100 | :type valid_from_date: :py:class:`datetime.datetime`
101 | :param valid_from_date: The new valid-from date.
102 |
103 | :type valid_to_date: :py:class:`datetime.datetime`
104 | :param valid_to_date: The new valid-to date.
105 | """
106 | verify_driver_from_license_number(driver, license_num)
107 | renew_drivers_license(driver, valid_from_date, valid_to_date, license_num)
108 |
109 |
110 | def main(ledger_name=Constants.LEDGER_NAME):
111 | """
112 | Find the person associated with a license number.
113 | Renew a driver's license.
114 | """
115 | try:
116 | with create_qldb_driver(ledger_name) as driver:
117 | license_number = SampleData.DRIVERS_LICENSE[0]['LicenseNumber']
118 | verify_and_renew_license(driver, license_number, VALID_FROM_DATE, VALID_TO_DATE)
119 | except Exception as e:
120 | logger.exception('Error renewing drivers license.')
121 | raise e
122 |
123 |
124 | if __name__ == '__main__':
125 | main()
126 |
--------------------------------------------------------------------------------
/pyqldbsamples/add_secondary_owner.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 |
21 | from pyqldbsamples.model.sample_data import to_ion_struct, get_document_ids, print_result, SampleData, \
22 | convert_object_to_ion
23 | from pyqldbsamples.constants import Constants
24 | from pyqldbsamples.connect_to_ledger import create_qldb_driver
25 |
26 | logger = getLogger(__name__)
27 | basicConfig(level=INFO)
28 |
29 |
30 | def get_document_id_by_gov_id(driver, government_id):
31 | """
32 | Find a driver's person ID using the given government ID.
33 |
34 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
35 | :param driver: An instance of the QldbDriver class.
36 |
37 | :type government_id: str
38 | :param government_id: A driver's government ID.
39 |
40 | :rtype: list
41 | :return: A list of document IDs.
42 | """
43 | logger.info("Finding secondary owner's person ID using given government ID: {}.".format(government_id))
44 | return driver.execute_lambda(lambda executor: get_document_ids(executor, Constants.PERSON_TABLE_NAME, 'GovId',
45 | government_id))
46 |
47 |
48 | def is_secondary_owner_for_vehicle(driver, vin, secondary_owner_id):
49 | """
50 | Check whether a secondary owner has already been registered for the given VIN.
51 |
52 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
53 | :param driver: An instance of the QldbDriver class.
54 |
55 | :type vin: str
56 | :param vin: VIN of the vehicle to query.
57 |
58 | :type secondary_owner_id: str
59 | :param secondary_owner_id: The secondary owner's person ID.
60 |
61 | :rtype: bool
62 | :return: If the driver has already been registered.
63 | """
64 | logger.info('Finding secondary owners for vehicle with VIN: {}...'.format(vin))
65 | query = 'SELECT Owners.SecondaryOwners FROM VehicleRegistration AS v WHERE v.VIN = ?'
66 | rows = driver.execute_lambda(lambda executor: executor.execute_statement(query, convert_object_to_ion(vin)))
67 |
68 | for row in rows:
69 | secondary_owners = row.get('SecondaryOwners')
70 | person_ids = map(lambda owner: owner.get('PersonId').text, secondary_owners)
71 | if secondary_owner_id in person_ids:
72 | return True
73 | return False
74 |
75 |
76 | def add_secondary_owner_for_vin(driver, vin, parameter):
77 | """
78 | Add a secondary owner into `VehicleRegistration` table for a particular VIN.
79 |
80 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
81 | :param driver: An instance of the QldbDriver class.
82 |
83 | :type vin: str
84 | :param vin: VIN of the vehicle to add a secondary owner for.
85 |
86 | :type parameter: :py:class:`amazon.ion.simple_types.IonPyValue`
87 | :param parameter: The Ion value or Python native type that is convertible to Ion for filling in parameters of the
88 | statement.
89 | """
90 | logger.info('Inserting secondary owner for vehicle with VIN: {}...'.format(vin))
91 | statement = "FROM VehicleRegistration AS v WHERE v.VIN = ? INSERT INTO v.Owners.SecondaryOwners VALUE ?"
92 |
93 | cursor = driver.execute_lambda(lambda executor: executor.execute_statement(statement, convert_object_to_ion(vin),
94 | parameter))
95 | logger.info('VehicleRegistration Document IDs which had secondary owners added: ')
96 | print_result(cursor)
97 |
98 |
99 | def register_secondary_owner(driver, vin, gov_id):
100 | """
101 | Register a secondary owner for a vehicle if they are not already registered.
102 |
103 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
104 | :param driver: An instance of the QldbDriver class.
105 |
106 | :type vin: str
107 | :param vin: VIN of the vehicle to register a secondary owner for.
108 |
109 | :type gov_id: str
110 | :param gov_id: The government ID of the owner.
111 | """
112 | logger.info('Finding the secondary owners for vehicle with VIN: {}.'.format(vin))
113 |
114 | document_ids = get_document_id_by_gov_id(driver, gov_id)
115 |
116 | for document_id in document_ids:
117 | if is_secondary_owner_for_vehicle(driver, vin, document_id):
118 | logger.info('Person with ID {} has already been added as a secondary owner of this vehicle.'.format(gov_id))
119 | else:
120 | add_secondary_owner_for_vin(driver, vin, to_ion_struct('PersonId', document_id))
121 |
122 |
123 | def main(ledger_name=Constants.LEDGER_NAME):
124 | """
125 | Finds and adds secondary owners for a vehicle.
126 | """
127 | vin = SampleData.VEHICLE[1]['VIN']
128 | gov_id = SampleData.PERSON[0]['GovId']
129 | try:
130 | with create_qldb_driver(ledger_name) as driver:
131 | register_secondary_owner(driver, vin, gov_id)
132 | logger.info('Secondary owners successfully updated.')
133 | except Exception as e:
134 | logger.exception('Error adding secondary owner.')
135 | raise e
136 |
137 |
138 | if __name__ == '__main__':
139 | main()
140 |
--------------------------------------------------------------------------------
/tests/test_integration.py:
--------------------------------------------------------------------------------
1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
4 | # the License. A copy of the License is located at
5 | #
6 | # http://www.apache.org/licenses/LICENSE-2.0
7 | #
8 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
9 | # CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
10 | # and limitations under the License.
11 | import sys
12 | from unittest import TestCase
13 |
14 | import pytest
15 | from pyqldbsamples.add_secondary_owner import main as add_secondary_owner_main
16 | from pyqldbsamples.connect_to_ledger import main as connect_to_ledger_main
17 | from pyqldbsamples.create_index import main as create_index_main
18 | from pyqldbsamples.create_ledger import main as create_ledger_main
19 | from pyqldbsamples.create_table import main as create_table_main
20 | from pyqldbsamples.deletion_protection import main as deletion_protection_main
21 | from pyqldbsamples.deregister_drivers_license import main as deregister_drivers_license_main
22 | from pyqldbsamples.describe_journal_export import main as describe_journal_export_main
23 | from pyqldbsamples.describe_ledger import main as describe_ledger_main
24 | from pyqldbsamples.export_journal import main as export_journal_main
25 | from pyqldbsamples.export_journal import create_export_role, set_up_s3_encryption_configuration
26 | from pyqldbsamples.find_vehicles import main as find_vehicles_main
27 | from pyqldbsamples.get_block import main as get_block_main
28 | from pyqldbsamples.get_digest import main as get_digest_main
29 | from pyqldbsamples.get_revision import main as get_revision_main
30 | from pyqldbsamples.insert_document import main as insert_document_main
31 | from pyqldbsamples.insert_ion_types import main as insert_ion_types_main
32 | from pyqldbsamples.list_journal_exports import main as list_journal_exports_main
33 | from pyqldbsamples.list_ledgers import main as list_ledgers_main
34 | from pyqldbsamples.list_tables import main as list_tables_main
35 | from pyqldbsamples.query_history import main as query_history_main
36 | from pyqldbsamples.redact_revision import main as redact_revision_main
37 | from pyqldbsamples.register_drivers_license import main as register_drivers_license_main
38 | from pyqldbsamples.renew_drivers_license import main as renew_drivers_license_main
39 | from pyqldbsamples.scan_table import main as scan_table_main
40 | from pyqldbsamples.tag_resource import main as tag_resource_main
41 | from pyqldbsamples.transfer_vehicle_ownership import main as transfer_vehicle_ownership_main
42 | from pyqldbsamples.validate_qldb_hash_chain import main as validate_qldb_hash_chain_main
43 | from tests.cleanup import get_deletion_ledger_name, get_ledger_name, get_role_name, get_role_policy_name, \
44 | get_s3_bucket_name, get_tag_ledger_name, delete_resources, poll_for_table_creation
45 |
46 |
47 | # The following tests only run the samples.
48 | @pytest.mark.usefixtures("config_variables")
49 | class TestIntegration(TestCase):
50 |
51 | @classmethod
52 | def setUpClass(cls):
53 | cls.role_name = get_role_name(cls.ledger_suffix)
54 | cls.role_policy_name = get_role_policy_name(cls.ledger_suffix)
55 | cls.s3_bucket_name = get_s3_bucket_name(cls.ledger_suffix)
56 | cls.ledger_name = get_ledger_name(cls.ledger_suffix)
57 | cls.deletion_ledger_name = get_deletion_ledger_name(cls.ledger_suffix)
58 | cls.tag_ledger_name = get_tag_ledger_name(cls.ledger_suffix)
59 |
60 | delete_resources(cls.ledger_suffix)
61 |
62 | s3_encryption_config = set_up_s3_encryption_configuration()
63 | cls.role_arn = create_export_role(cls.role_name, s3_encryption_config.get('KmsKeyArn'), cls.role_policy_name,
64 | cls.s3_bucket_name)
65 |
66 | create_ledger_main(cls.ledger_name)
67 | create_table_main(cls.ledger_name)
68 | poll_for_table_creation(cls.ledger_name)
69 | create_index_main(cls.ledger_name)
70 | insert_document_main(cls.ledger_name)
71 |
72 | @classmethod
73 | def tearDownClass(cls):
74 | delete_resources(cls.ledger_suffix)
75 |
76 | def test_list_ledgers(self):
77 | list_ledgers_main()
78 |
79 | def test_connect_to_ledger(self):
80 | connect_to_ledger_main(self.ledger_name)
81 |
82 | def test_insert_ion_types(self):
83 | insert_ion_types_main(self.ledger_name)
84 |
85 | def test_scan_table(self):
86 | scan_table_main(self.ledger_name)
87 |
88 | def test_find_vehicles(self):
89 | find_vehicles_main(self.ledger_name)
90 |
91 | def test_add_secondary_owner(self):
92 | add_secondary_owner_main(self.ledger_name)
93 |
94 | def test_deregister_drivers_license(self):
95 | deregister_drivers_license_main(self.ledger_name)
96 |
97 | def test_export_journal_and_describe_journal_export_and_validate_qldb_hash(self):
98 | sys.argv[1:] = [self.s3_bucket_name, self.role_arn]
99 | export_id = export_journal_main(self.ledger_name).get('ExportId')
100 |
101 | sys.argv[1:] = [export_id]
102 | describe_journal_export_main(self.ledger_name)
103 |
104 | sys.argv[1:] = [export_id]
105 | validate_qldb_hash_chain_main(self.ledger_name)
106 |
107 | def test_describe_ledger(self):
108 | describe_ledger_main(self.ledger_name)
109 |
110 | def test_transfer_vehicle_ownership(self):
111 | transfer_vehicle_ownership_main(self.ledger_name)
112 |
113 | def test_query_history(self):
114 | query_history_main(self.ledger_name)
115 |
116 | def test_redact_revision(self):
117 | redact_revision_main(self.ledger_name)
118 |
119 | def test_list_tables(self):
120 | list_tables_main(self.ledger_name)
121 |
122 | def test_register_drivers_license(self):
123 | register_drivers_license_main(self.ledger_name)
124 |
125 | def test_renew_drivers_license(self):
126 | renew_drivers_license_main(self.ledger_name)
127 |
128 | def test_deletion_protection(self):
129 | deletion_protection_main(self.deletion_ledger_name)
130 |
131 | def test_list_journal_exports(self):
132 | list_journal_exports_main(self.ledger_name)
133 |
134 | def test_get_revision(self):
135 | get_revision_main(self.ledger_name)
136 |
137 | def test_get_block(self):
138 | get_block_main(self.ledger_name)
139 |
140 | def test_get_digest(self):
141 | get_digest_main(self.ledger_name)
142 |
143 | def test_tag_resource(self):
144 | tag_resource_main(self.tag_ledger_name)
145 |
--------------------------------------------------------------------------------
/pyqldbsamples/transfer_vehicle_ownership.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 |
21 | from pyqldbsamples.add_secondary_owner import get_document_ids, print_result, SampleData
22 | from pyqldbsamples.constants import Constants
23 | from pyqldbsamples.model.sample_data import convert_object_to_ion
24 | from pyqldbsamples.connect_to_ledger import create_qldb_driver
25 |
26 | logger = getLogger(__name__)
27 | basicConfig(level=INFO)
28 |
29 |
30 | def find_person_from_document_id(transaction_executor, document_id):
31 | """
32 | Query a driver's information using the given ID.
33 |
34 | :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
35 | :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
36 |
37 | :type document_id: :py:class:`amazon.ion.simple_types.IonPyText`
38 | :param document_id: The document ID required to query for the person.
39 |
40 | :rtype: :py:class:`amazon.ion.simple_types.IonPyDict`
41 | :return: The resulting document from the query.
42 | """
43 | query = 'SELECT p.* FROM Person AS p BY pid WHERE pid = ?'
44 | cursor = transaction_executor.execute_statement(query, document_id)
45 | return next(cursor)
46 |
47 |
48 | def find_primary_owner_for_vehicle(driver, vin):
49 | """
50 | Find the primary owner of a vehicle given its VIN.
51 |
52 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
53 | :param driver: An instance of the QldbDriver class.
54 |
55 | :type vin: str
56 | :param vin: The VIN to find primary owner for.
57 |
58 | :rtype: :py:class:`amazon.ion.simple_types.IonPyDict`
59 | :return: The resulting document from the query.
60 | """
61 | logger.info('Finding primary owner for vehicle with VIN: {}.'.format(vin))
62 | query = "SELECT Owners.PrimaryOwner.PersonId FROM VehicleRegistration AS v WHERE v.VIN = ?"
63 | cursor = driver.execute_lambda(lambda executor: executor.execute_statement(query, convert_object_to_ion(vin)))
64 | try:
65 | return driver.execute_lambda(lambda executor: find_person_from_document_id(executor,
66 | next(cursor).get('PersonId')))
67 | except StopIteration:
68 | logger.error('No primary owner registered for this vehicle.')
69 | return None
70 |
71 |
72 | def update_vehicle_registration(driver, vin, document_id):
73 | """
74 | Update the primary owner for a vehicle using the given VIN.
75 |
76 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
77 | :param driver: An instance of the QldbDriver class.
78 |
79 | :type vin: str
80 | :param vin: The VIN for the vehicle to operate on.
81 |
82 | :type document_id: :py:class:`amazon.ion.simple_types.IonPyText`
83 | :param document_id: New PersonId for the primary owner.
84 |
85 | :raises RuntimeError: If no vehicle registration was found using the given document ID and VIN.
86 | """
87 | logger.info('Updating the primary owner for vehicle with Vin: {}...'.format(vin))
88 | statement = "UPDATE VehicleRegistration AS r SET r.Owners.PrimaryOwner.PersonId = ? WHERE r.VIN = ?"
89 | cursor = driver.execute_lambda(lambda executor: executor.execute_statement(statement, document_id,
90 | convert_object_to_ion(vin)))
91 | try:
92 | print_result(cursor)
93 | logger.info('Successfully transferred vehicle with VIN: {} to new owner.'.format(vin))
94 | except StopIteration:
95 | raise RuntimeError('Unable to transfer vehicle, could not find registration.')
96 |
97 |
98 | def validate_and_update_registration(driver, vin, current_owner, new_owner):
99 | """
100 | Validate the current owner of the given vehicle and transfer its ownership to a new owner.
101 |
102 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
103 | :param driver: An instance of the QldbDriver class.
104 |
105 | :type vin: str
106 | :param vin: The VIN of the vehicle to transfer ownership of.
107 |
108 | :type current_owner: str
109 | :param current_owner: The GovId of the current owner of the vehicle.
110 |
111 | :type new_owner: str
112 | :param new_owner: The GovId of the new owner of the vehicle.
113 |
114 | :raises RuntimeError: If unable to verify primary owner.
115 | """
116 | primary_owner = find_primary_owner_for_vehicle(driver, vin)
117 | if primary_owner is None or primary_owner['GovId'] != current_owner:
118 | raise RuntimeError('Incorrect primary owner identified for vehicle, unable to transfer.')
119 |
120 | document_ids = driver.execute_lambda(lambda executor: get_document_ids(executor, Constants.PERSON_TABLE_NAME,
121 | 'GovId', new_owner))
122 | update_vehicle_registration(driver, vin, document_ids[0])
123 |
124 |
125 | def main(ledger_name=Constants.LEDGER_NAME):
126 | """
127 | Find primary owner for a particular vehicle's VIN.
128 | Transfer to another primary owner for a particular vehicle's VIN.
129 | """
130 | vehicle_vin = SampleData.VEHICLE[0]['VIN']
131 | previous_owner = SampleData.PERSON[0]['GovId']
132 | new_owner = SampleData.PERSON[1]['GovId']
133 |
134 | try:
135 | with create_qldb_driver(ledger_name) as driver:
136 | validate_and_update_registration(driver, vehicle_vin, previous_owner, new_owner)
137 | except Exception as e:
138 | logger.exception('Error updating VehicleRegistration.')
139 | raise e
140 |
141 |
142 | if __name__ == '__main__':
143 | main()
144 |
--------------------------------------------------------------------------------
/pyqldbsamples/get_revision.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 |
21 | from amazon.ion.simpleion import loads
22 | from boto3 import client
23 |
24 | from pyqldbsamples.constants import Constants
25 | from pyqldbsamples.get_digest import get_digest_result
26 | from pyqldbsamples.model.sample_data import SampleData, convert_object_to_ion
27 | from pyqldbsamples.qldb.block_address import block_address_to_dictionary
28 | from pyqldbsamples.verifier import verify_document, flip_random_bit, to_base_64
29 | from pyqldbsamples.connect_to_ledger import create_qldb_driver
30 | from pyqldbsamples.qldb.qldb_string_utils import value_holder_to_string
31 |
32 | logger = getLogger(__name__)
33 | basicConfig(level=INFO)
34 | qldb_client = client('qldb')
35 |
36 |
37 | def get_revision(ledger_name, document_id, block_address, digest_tip_address):
38 | """
39 | Get the revision data object for a specified document ID and block address.
40 | Also returns a proof of the specified revision for verification.
41 |
42 | :type ledger_name: str
43 | :param ledger_name: Name of the ledger containing the document to query.
44 |
45 | :type document_id: str
46 | :param document_id: Unique ID for the document to be verified, contained in the committed view of the document.
47 |
48 | :type block_address: dict
49 | :param block_address: The location of the block to request.
50 |
51 | :type digest_tip_address: dict
52 | :param digest_tip_address: The latest block location covered by the digest.
53 |
54 | :rtype: dict
55 | :return: The response of the request.
56 | """
57 | result = qldb_client.get_revision(Name=ledger_name, BlockAddress=block_address, DocumentId=document_id,
58 | DigestTipAddress=digest_tip_address)
59 | return result
60 |
61 |
62 | def lookup_registration_for_vin(driver, vin):
63 | """
64 | Query revision history for a particular vehicle for verification.
65 |
66 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
67 | :param driver: An instance of the QldbDriver class.
68 |
69 | :type vin: str
70 | :param vin: VIN to query the revision history of a specific registration with.
71 |
72 | :rtype: :py:class:`pyqldb.cursor.buffered_cursor.BufferedCursor`
73 | :return: Cursor on the result set of the statement query.
74 | """
75 | logger.info("Querying the 'VehicleRegistration' table for VIN: {}...".format(vin))
76 | query = 'SELECT * FROM _ql_committed_VehicleRegistration WHERE data.VIN = ?'
77 | return driver.execute_lambda(lambda txn: txn.execute_statement(query, convert_object_to_ion(vin)))
78 |
79 |
80 | def verify_registration(driver, ledger_name, vin):
81 | """
82 | Verify each version of the registration for the given VIN.
83 |
84 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
85 | :param driver: An instance of the QldbDriver class.
86 |
87 | :type ledger_name: str
88 | :param ledger_name: The ledger to get digest from.
89 |
90 | :type vin: str
91 | :param vin: VIN to query the revision history of a specific registration with.
92 |
93 | :raises AssertionError: When verification failed.
94 | """
95 | logger.info("Let's verify the registration with VIN = {}, in ledger = {}.".format(vin, ledger_name))
96 | digest = get_digest_result(ledger_name)
97 | digest_bytes = digest.get('Digest')
98 | digest_tip_address = digest.get('DigestTipAddress')
99 |
100 | logger.info('Got a ledger digest: digest tip address = {}, digest = {}.'.format(
101 | value_holder_to_string(digest_tip_address.get('IonText')), to_base_64(digest_bytes)))
102 |
103 | logger.info('Querying the registration with VIN = {} to verify each version of the registration...'.format(vin))
104 | cursor = lookup_registration_for_vin(driver, vin)
105 | logger.info('Getting a proof for the document.')
106 |
107 | for row in cursor:
108 | block_address = row.get('blockAddress')
109 | document_id = row.get('metadata').get('id')
110 |
111 | result = get_revision(ledger_name, document_id, block_address_to_dictionary(block_address), digest_tip_address)
112 | revision = result.get('Revision').get('IonText')
113 | document_hash = loads(revision).get('hash')
114 |
115 | proof = result.get('Proof')
116 | logger.info('Got back a proof: {}.'.format(proof))
117 |
118 | verified = verify_document(document_hash, digest_bytes, proof)
119 | if not verified:
120 | raise AssertionError('Document revision is not verified.')
121 | else:
122 | logger.info('Success! The document is verified.')
123 |
124 | altered_document_hash = flip_random_bit(document_hash)
125 | logger.info("Flipping one bit in the document's hash and assert that the document is NOT verified. "
126 | "The altered document hash is: {}.".format(to_base_64(altered_document_hash)))
127 | verified = verify_document(altered_document_hash, digest_bytes, proof)
128 | if verified:
129 | raise AssertionError('Expected altered document hash to not be verified against digest.')
130 | else:
131 | logger.info('Success! As expected flipping a bit in the document hash causes verification to fail.')
132 |
133 | logger.info('Finished verifying the registration with VIN = {} in ledger = {}.'.format(vin, ledger_name))
134 |
135 |
136 | def main(ledger_name=Constants.LEDGER_NAME):
137 | """
138 | Verify the integrity of a document revision in a QLDB ledger.
139 | """
140 | registration = SampleData.VEHICLE_REGISTRATION[0]
141 | vin = registration['VIN']
142 | try:
143 | with create_qldb_driver(ledger_name) as driver:
144 | verify_registration(driver, ledger_name, vin)
145 | except Exception as e:
146 | logger.exception('Unable to verify revision.')
147 | raise e
148 |
149 |
150 | if __name__ == '__main__':
151 | main()
152 |
--------------------------------------------------------------------------------
/pyqldbsamples/verifier.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from array import array
20 | from base64 import b64encode
21 | from functools import reduce
22 | from hashlib import sha256
23 | from random import randrange
24 |
25 | from amazon.ion.simpleion import loads
26 |
27 | HASH_LENGTH = 32
28 | UPPER_BOUND = 8
29 |
30 |
31 | def parse_proof(value_holder):
32 | """
33 | Parse the Proof object returned by QLDB into an iterator.
34 |
35 | The Proof object returned by QLDB is a dictionary like the following:
36 | {'IonText': '[{{}},{{}}]'}
37 |
38 | :type value_holder: dict
39 | :param value_holder: A structure containing an Ion string value.
40 |
41 | :rtype: :py:class:`amazon.ion.simple_types.IonPyList`
42 | :return: A list of hash values.
43 | """
44 | value_holder = value_holder.get('IonText')
45 | proof_list = loads(value_holder)
46 | return proof_list
47 |
48 |
49 | def parse_block(value_holder):
50 | """
51 | Parse the Block object returned by QLDB and retrieve block hash.
52 |
53 | :type value_holder: dict
54 | :param value_holder: A structure containing an Ion string value.
55 |
56 | :rtype: :py:class:`amazon.ion.simple_types.IonPyBytes`
57 | :return: The block hash.
58 | """
59 | value_holder = value_holder.get('IonText')
60 | block = loads(value_holder)
61 | block_hash = block.get('blockHash')
62 | return block_hash
63 |
64 |
65 | def flip_random_bit(original):
66 | """
67 | Flip a single random bit in the given hash value.
68 | This method is used to demonstrate QLDB's verification features.
69 |
70 | :type original: bytes
71 | :param original: The hash value to alter.
72 |
73 | :rtype: bytes
74 | :return: The altered hash with a single random bit changed.
75 | """
76 | assert len(original) != 0, 'Invalid bytes.'
77 |
78 | altered_position = randrange(len(original))
79 | bit_shift = randrange(UPPER_BOUND)
80 | altered_hash = bytearray(original).copy()
81 |
82 | altered_hash[altered_position] = altered_hash[altered_position] ^ (1 << bit_shift)
83 | return bytes(altered_hash)
84 |
85 |
86 | def compare_hash_values(hash1, hash2):
87 | """
88 | Compare two hash values by converting them into byte arrays, assuming they are little endian.
89 |
90 | :type hash1: bytes
91 | :param hash1: The hash value to compare.
92 |
93 | :type hash2: bytes
94 | :param hash2: The hash value to compare.
95 |
96 | :rtype: int
97 | :return: Zero if the hash values are equal, otherwise return the difference of the first pair of non-matching bytes.
98 | """
99 | assert len(hash1) == HASH_LENGTH
100 | assert len(hash2) == HASH_LENGTH
101 |
102 | hash_array1 = array('b', hash1)
103 | hash_array2 = array('b', hash2)
104 |
105 | for i in range(len(hash_array1) - 1, -1, -1):
106 | difference = hash_array1[i] - hash_array2[i]
107 | if difference != 0:
108 | return difference
109 | return 0
110 |
111 |
112 | def join_hash_pairwise(hash1, hash2):
113 | """
114 | Take two hash values, sort them, concatenate them, and generate a new hash value from the concatenated values.
115 |
116 | :type hash1: bytes
117 | :param hash1: Hash value to concatenate.
118 |
119 | :type hash2: bytes
120 | :param hash2: Hash value to concatenate.
121 |
122 | :rtype: bytes
123 | :return: The new hash value generated from concatenated hash values.
124 | """
125 | if len(hash1) == 0:
126 | return hash2
127 | if len(hash2) == 0:
128 | return hash1
129 |
130 | concatenated = hash1 + hash2 if compare_hash_values(hash1, hash2) < 0 else hash2 + hash1
131 | new_hash_lib = sha256()
132 | new_hash_lib.update(concatenated)
133 | new_digest = new_hash_lib.digest()
134 | return new_digest
135 |
136 |
137 | def calculate_root_hash_from_internal_hashes(internal_hashes, leaf_hash):
138 | """
139 | Combine the internal hashes and the leaf hash until only one root hash remains.
140 |
141 | :type internal_hashes: map
142 | :param internal_hashes: An iterable over a list of hash values.
143 |
144 | :type leaf_hash: bytes
145 | :param leaf_hash: The revision hash to pair with the first hash in the Proof hashes list.
146 |
147 | :rtype: bytes
148 | :return: The root hash constructed by combining internal hashes.
149 | """
150 | root_hash = reduce(join_hash_pairwise, internal_hashes, leaf_hash)
151 | return root_hash
152 |
153 |
154 | def build_candidate_digest(proof, leaf_hash):
155 | """
156 | Build the candidate digest representing the entire ledger from the Proof hashes.
157 |
158 | :type proof: dict
159 | :param proof: The Proof object.
160 |
161 | :type leaf_hash: bytes
162 | :param leaf_hash: The revision hash to pair with the first hash in the Proof hashes list.
163 |
164 | :rtype: bytes
165 | :return: The calculated root hash.
166 | """
167 | parsed_proof = parse_proof(proof)
168 | root_hash = calculate_root_hash_from_internal_hashes(parsed_proof, leaf_hash)
169 | return root_hash
170 |
171 |
172 | def verify_document(document_hash, digest, proof):
173 | """
174 | Verify document revision against the provided digest.
175 |
176 | :type document_hash: bytes
177 | :param document_hash: The SHA-256 value representing the document revision to be verified.
178 |
179 | :type digest: bytes
180 | :param digest: The SHA-256 hash value representing the ledger digest.
181 |
182 | :type proof: dict
183 | :param proof: The Proof object retrieved from :func:`pyqldbsamples.get_revision.get_revision`.
184 |
185 | :rtype: bool
186 | :return: If the document revision verify against the ledger digest.
187 | """
188 | candidate_digest = build_candidate_digest(proof, document_hash)
189 | return digest == candidate_digest
190 |
191 |
192 | def to_base_64(input):
193 | """
194 | Encode input in base64.
195 |
196 | :type input: bytes
197 | :param input: Input to be encoded.
198 |
199 | :rtype: string
200 | :return: Return input that has been encoded in base64.
201 | """
202 | encoded_value = b64encode(input)
203 | return str(encoded_value, 'UTF-8')
204 |
--------------------------------------------------------------------------------
/pyqldbsamples/get_block.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 |
21 | from boto3 import client
22 |
23 | from pyqldbsamples.constants import Constants
24 | from pyqldbsamples.model.sample_data import SampleData
25 | from pyqldbsamples.get_revision import lookup_registration_for_vin
26 | from pyqldbsamples.get_digest import get_digest_result
27 | from pyqldbsamples.verifier import to_base_64, verify_document, parse_block, flip_random_bit
28 | from pyqldbsamples.qldb.block_address import block_address_to_dictionary
29 | from pyqldbsamples.connect_to_ledger import create_qldb_driver
30 | from pyqldbsamples.qldb.qldb_string_utils import block_response_to_string, value_holder_to_string
31 |
32 | logger = getLogger(__name__)
33 | basicConfig(level=INFO)
34 | qldb_client = client('qldb')
35 |
36 |
37 | def get_block(ledger_name, block_address):
38 | """
39 | Get the block of a ledger's journal.
40 |
41 | :type ledger_name: str
42 | :param ledger_name: Name of the ledger to operate on.
43 |
44 | :type block_address: dict
45 | :param block_address: The location of the block to request.
46 |
47 | :rtype: dict
48 | :return: The response of the request.
49 | """
50 | logger.info("Let's get the block for block address {} of the ledger named {}.".format(block_address, ledger_name))
51 | result = qldb_client.get_block(Name=ledger_name, BlockAddress=block_address)
52 | logger.info('Success. GetBlock: {}'.format(block_response_to_string(result)))
53 | return result
54 |
55 |
56 | def get_block_with_proof(ledger_name, block_address, digest_tip_address):
57 | """
58 | Get the block of a ledger's journal. Also returns a proof of the block for verification.
59 |
60 | :type ledger_name: str
61 | :param ledger_name: Name of the ledger to operate on.
62 |
63 | :type block_address: dict
64 | :param block_address: The location of the block to request.
65 |
66 | :type digest_tip_address: dict
67 | :param digest_tip_address: The location of the digest tip.
68 |
69 | :rtype: dict
70 | :return: The response of the request.
71 | """
72 | logger.info("Let's get the block for block address {}, digest tip address {}, for the ledger named {}.".format(
73 | block_address, digest_tip_address, ledger_name))
74 | result = qldb_client.get_block(Name=ledger_name, BlockAddress=block_address,
75 | DigestTipAddress=digest_tip_address)
76 | logger.info('Success. GetBlock: {}.'.format(block_response_to_string(result)))
77 | return result
78 |
79 |
80 | def verify_block(ledger_name, block_address):
81 | """
82 | Verify block by validating the proof returned in the getBlock response.
83 |
84 | :type ledger_name: str
85 | :param ledger_name: The ledger to get digest from.
86 |
87 | :type block_address: str/:py:class:`amazon.ion.simple_types.IonPyDict`
88 | :param block_address: The address of the block to verify.
89 |
90 | :raises AssertionError: When verification failed.
91 | """
92 | logger.info("Let's verify blocks for ledger with name={}.".format(ledger_name))
93 |
94 | try:
95 | logger.info("First, let's get a digest.")
96 | digest_result = get_digest_result(ledger_name)
97 |
98 | digest_tip_address = digest_result.get('DigestTipAddress')
99 | digest_bytes = digest_result.get('Digest')
100 |
101 | logger.info('Got a ledger digest. Digest end address={}, digest={}'.format(
102 | value_holder_to_string(digest_tip_address.get('IonText')), to_base_64(digest_bytes)))
103 | get_block_result = get_block_with_proof(ledger_name, block_address_to_dictionary(block_address),
104 | digest_tip_address)
105 | block = get_block_result.get('Block')
106 | block_hash = parse_block(block)
107 |
108 | verified = verify_document(block_hash, digest_bytes, get_block_result.get('Proof'))
109 |
110 | if not verified:
111 | raise AssertionError('Block is not verified!')
112 | else:
113 | logger.info('Success! The block is verified.')
114 |
115 | altered_digest = flip_random_bit(digest_bytes)
116 | logger.info("Let's try flipping one bit in the digest and assert that the block is NOT verified. "
117 | "The altered digest is: {}".format(to_base_64(altered_digest)))
118 |
119 | verified = verify_document(block_hash, altered_digest, get_block_result.get('Proof'))
120 |
121 | if verified:
122 | raise AssertionError('Expected block to not be verified against altered digest.')
123 | else:
124 | logger.info('Success! As expected flipping a bit in the digest causes verification to fail.')
125 |
126 | altered_block_hash = flip_random_bit(block_hash)
127 | logger.info("Let's try flipping one bit in the block's hash and assert that the block is NOT verified. "
128 | "The altered block hash is: {}.".format(to_base_64(altered_block_hash)))
129 |
130 | verified = verify_document(altered_block_hash, digest_bytes, get_block_result.get('Proof'))
131 |
132 | if verified:
133 | raise AssertionError('Expected altered block hash to not be verified against digest.')
134 | else:
135 | logger.info('Success! As expected flipping a bit in the block hash causes verification to fail.')
136 | except Exception as e:
137 | logger.exception('Failed to verify blocks in the ledger with name={}.'.format(ledger_name))
138 | raise e
139 |
140 |
141 | def main(ledger_name=Constants.LEDGER_NAME):
142 | """
143 | Get a journal block from a QLDB ledger.
144 |
145 | After getting the block, we get the digest of the ledger and validate the
146 | proof returned in the getBlock response.
147 | """
148 | vin = SampleData.VEHICLE_REGISTRATION[1]['VIN']
149 | try:
150 | with create_qldb_driver(ledger_name) as driver:
151 | cursor = lookup_registration_for_vin(driver, vin)
152 | row = next(cursor)
153 | block_address = row.get('blockAddress')
154 | verify_block(ledger_name, block_address)
155 | except Exception as e:
156 | logger.exception('Unable to query vehicle registration by Vin.')
157 | raise e
158 |
159 |
160 | if __name__ == '__main__':
161 | main()
162 |
--------------------------------------------------------------------------------
/pyqldbsamples/register_drivers_license.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 | from datetime import datetime
21 |
22 | from pyqldbsamples.model.sample_data import convert_object_to_ion, get_document_ids
23 | from pyqldbsamples.constants import Constants
24 | from pyqldbsamples.insert_document import insert_documents
25 | from pyqldbsamples.connect_to_ledger import create_qldb_driver
26 |
27 | logger = getLogger(__name__)
28 | basicConfig(level=INFO)
29 |
30 |
31 | def person_already_exists(transaction_executor, gov_id):
32 | """
33 | Verify whether a driver already exists in the database.
34 |
35 | :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
36 | :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
37 |
38 | :type gov_id: str
39 | :param gov_id: The government ID to search `Person` table against.
40 |
41 | :rtype: bool
42 | :return: If the Person has been registered.
43 | """
44 | query = 'SELECT * FROM Person AS p WHERE p.GovId = ?'
45 | cursor = transaction_executor.execute_statement(query, convert_object_to_ion(gov_id))
46 | try:
47 | next(cursor)
48 | return True
49 | except StopIteration:
50 | return False
51 |
52 |
53 | def person_has_drivers_license(transaction_executor, document_id):
54 | """
55 | Check if the driver already has a driver's license using their unique document ID.
56 |
57 | :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
58 | :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
59 |
60 | :type document_id: str
61 | :param document_id: The document ID to check.
62 |
63 | :rtype: bool
64 | :return: If the Person has a drivers license.
65 | """
66 | cursor = lookup_drivers_license_for_person(transaction_executor, document_id)
67 | try:
68 | next(cursor)
69 | return True
70 | except StopIteration:
71 | return False
72 |
73 |
74 | def lookup_drivers_license_for_person(transaction_executor, person_id):
75 | """
76 | Query drivers license table by person ID.
77 |
78 | :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
79 | :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
80 |
81 | :type person_id: str
82 | :param person_id: The person ID to check.
83 |
84 | :rtype: :py:class:`pyqldb.cursor.stream_cursor.StreamCursor`
85 | :return: Cursor on the result set of a statement query.
86 | """
87 | query = 'SELECT * FROM DriversLicense AS d WHERE d.PersonId = ?'
88 | cursor = transaction_executor.execute_statement(query, person_id)
89 | return cursor
90 |
91 |
92 | def register_new_person(driver, person):
93 | """
94 | Register a new person in QLDB if not already registered.
95 |
96 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
97 | :param driver: An instance of the QldbDriver class.
98 |
99 | :type person: dict
100 | :param person: The person to register.
101 |
102 | :rtype: str
103 | :return: The person ID.
104 | """
105 | gov_id = person['GovId']
106 | if driver.execute_lambda(lambda executor: person_already_exists(executor, gov_id)):
107 | logger.info('Person with this GovId already exists.')
108 |
109 | result = driver.execute_lambda(lambda executor: get_document_ids(executor, Constants.PERSON_TABLE_NAME,
110 | 'GovId', gov_id))
111 | result = result[0]
112 | else:
113 | result = insert_documents(driver, Constants.PERSON_TABLE_NAME, [person])
114 | result = result[0]
115 | return result
116 |
117 |
118 | def register_new_drivers_license(driver, person, new_license):
119 | """
120 | Register a new person and a new driver's license.
121 |
122 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
123 | :param driver: An instance of the QldbDriver class.
124 |
125 | :type person: dict
126 | :param person: The person to register.
127 |
128 | :type new_license: dict
129 | :param new_license: The driver's license to register.
130 | """
131 | person_id = register_new_person(driver, person)
132 | if driver.execute_lambda(lambda executor: person_has_drivers_license(executor, person_id)):
133 | gov_id = person['GovId']
134 | logger.info("Person with government ID '{}' already has a license! No new license added.".format(gov_id))
135 | else:
136 | logger.info("Registering new driver's license...")
137 | # Update the new license with new driver's unique PersonId.
138 | new_license.update({'PersonId': str(person_id)})
139 | statement = 'INSERT INTO DriversLicense ?'
140 | driver.execute_lambda(lambda executor: executor.execute_statement(statement, convert_object_to_ion(new_license)))
141 |
142 | cursor = driver.execute_lambda(lambda executor: lookup_drivers_license_for_person(executor, person_id))
143 | try:
144 | next(cursor)
145 | logger.info('Successfully registered new driver.')
146 | return
147 | except StopIteration:
148 | logger.info('Problem occurred while inserting new license, please review the results.')
149 | return
150 |
151 |
152 | def main(ledger_name=Constants.LEDGER_NAME):
153 | """
154 | Register a new driver's license.
155 | """
156 | try:
157 | with create_qldb_driver(ledger_name) as driver:
158 | person = {
159 | 'FirstName': 'Kate',
160 | 'LastName': 'Mulberry',
161 | 'Address': '22 Commercial Drive, Blaine, WA, 97722',
162 | 'DOB': datetime(1995, 2, 9),
163 | 'GovId': 'AQQ17B2342',
164 | 'GovIdType': 'Passport'
165 | }
166 | drivers_license = {
167 | 'PersonId': '',
168 | 'LicenseNumber': '112 360 PXJ',
169 | 'LicenseType': 'Full',
170 | 'ValidFromDate': datetime(2018, 6, 30),
171 | 'ValidToDate': datetime(2022, 10, 30)
172 | }
173 |
174 | register_new_drivers_license(driver, person, drivers_license)
175 | except Exception as e:
176 | logger.exception('Error registering new driver.')
177 | raise e
178 |
179 |
180 | if __name__ == '__main__':
181 | main()
182 |
--------------------------------------------------------------------------------
/pyqldbsamples/journal_s3_export_reader.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from json import dumps as json_dumps
20 | from logging import getLogger
21 |
22 | from amazon.ion.simpleion import loads
23 |
24 | from pyqldbsamples.qldb.journal_block import from_ion
25 |
26 | logger = getLogger(__name__)
27 |
28 |
29 | def compare_key_with_content_range(file_key, first_block, last_block):
30 | """
31 | Compare the expected block range, derived from File Key, with the actual object content.
32 |
33 | :type file_key: str
34 | :param file_key: The key of data file containing the chunk of journal block. The fileKey pattern is
35 | {[strandId].[firstSequenceNo]-[lastSequenceNo].ion}.
36 |
37 | :type first_block: :py:class:`pyqldbsamples.qldb.journal_block.JournalBlock`
38 | :param first_block: The first block in the block chain for a particular journal strand.
39 |
40 | :type last_block: :py:class:`pyqldbsamples.qldb.journal_block.JournalBlock`
41 | :param last_block: The last block in the block chain for a particular journal strand.
42 |
43 | :raises RuntimeError: If the SequenceNo on the blockAddress does not match the expected SequenceNo.
44 | """
45 | sequence_no_range = file_key.split(".")[1]
46 | key_tokens = sequence_no_range.split("-")
47 | start_sequence_no = key_tokens[0]
48 | last_sequence_no = key_tokens[1]
49 |
50 | if str(first_block.block_address.get("sequenceNo")) != start_sequence_no:
51 | raise RuntimeError('Expected first block SequenceNo to be {}.'.format(start_sequence_no))
52 | if str(last_block.block_address.get("sequenceNo")) != last_sequence_no:
53 | raise RuntimeError('Expected last block SequenceNo to be {}.'.format(last_sequence_no))
54 |
55 |
56 | def filter_for_initial_manifest(objects, manifest):
57 | """
58 | Find the initial manifest created at the beginning of a export request.
59 |
60 | :type objects: list
61 | :param objects: List of objects in a particular bucket.
62 |
63 | :type manifest: str
64 | :param manifest: The expected identifier for the initial manifest.
65 |
66 | :rtype: str
67 | :return: The identifier for the initial manifest object.
68 |
69 | :raises RuntimeError: If the initial manifest is not found.
70 | """
71 | for obj in objects:
72 | key = obj['Key'].casefold()
73 | if key == manifest.casefold():
74 | return key
75 | raise RuntimeError('Initial manifest not found.')
76 |
77 |
78 | def filter_for_completed_manifest(objects):
79 | """
80 | Find the final manifest objects created after the completion of an export job.
81 |
82 | :type objects: list
83 | :param objects: List of objects in a particular bucket.
84 |
85 | :rtype: str
86 | :return: The identifier for the final manifest object.
87 |
88 | :raises RuntimeError: If the final manifest is not found.
89 | """
90 | for obj in objects:
91 | key = obj['Key']
92 | if key.casefold().endswith("completed.manifest"):
93 | return key
94 | raise RuntimeError('Completed manifest not found.')
95 |
96 |
97 | def get_data_file_keys_from_manifest(manifest_object):
98 | """
99 | Retrieve the ordered list of data object keys within the given final manifest.
100 |
101 | :type manifest_object: str
102 | :param manifest_object: The content of the final manifest.
103 |
104 | :rtype: list
105 | :return: List of data object keys.
106 | """
107 | ion_keys = loads(manifest_object).get('keys')
108 | list_of_keys = list(ion_keys)
109 | return list_of_keys
110 |
111 |
112 | def get_journal_blocks(s3_object):
113 | """
114 | Parse the given S3 object's content for the journal data objects in Ion format.
115 |
116 | :type s3_object: str
117 | :param s3_object: The content within a S3 object as an ion string.
118 |
119 | :rtype: list
120 | :return: List of journal blocks.
121 |
122 | :raises RuntimeError: If there is an error loading the journal.
123 | """
124 | journals = loads(s3_object, single_value=False)
125 | journal_blocks = []
126 |
127 | try:
128 | for journal in journals:
129 | parsed_journal = from_ion(journal)
130 | journal_blocks.append(parsed_journal)
131 | except Exception:
132 | logger.log("Invalid format to map to a JournalBlock!")
133 | raise Exception
134 |
135 | logger.info('Found {} block(s).'.format(len(journal_blocks)))
136 | return journal_blocks
137 |
138 |
139 | def read_export(describe_journal_export_result, s3_client):
140 | """
141 | Read the S3 export within a journal block.
142 |
143 | :type describe_journal_export_result: dict
144 | :param describe_journal_export_result: The result from QLDB describing a journal export.
145 |
146 | :type s3_client: :py:class:`botocore.client.BaseClient`
147 | :param s3_client: The low-level S3 client.
148 |
149 | :rtype: list
150 | :return: List of journal blocks.
151 | """
152 | export_configuration = describe_journal_export_result.get('S3ExportConfiguration')
153 | prefix = export_configuration.get('Prefix')
154 | bucket_name = export_configuration.get('Bucket')
155 | response = s3_client.list_objects_v2(Bucket=bucket_name, Prefix=prefix)
156 | objects = response['Contents']
157 | logger.info('Found the following objects for list from S3:')
158 | for obj in objects:
159 | logger.info(obj['Key'])
160 |
161 | # Validate initial manifest file was written
162 | expected_manifest_key = "{}{}.started.manifest".format(prefix, describe_journal_export_result.get('ExportId'))
163 | initial_manifest = filter_for_initial_manifest(objects, expected_manifest_key)
164 | logger.info('Found the initial manifest with key: {}.'.format(initial_manifest))
165 |
166 | # Find the final manifest file, it should contain the exportId in it.
167 | completed_manifest_file_key = filter_for_completed_manifest(objects)
168 | completed_manifest_object = s3_client.get_object(Bucket=bucket_name, Key=completed_manifest_file_key)['Body']\
169 | .read().decode('utf-8')
170 |
171 | data_file_keys = get_data_file_keys_from_manifest(completed_manifest_object)
172 |
173 | logger.info('Found the following keys in the manifest files: {}.'.format(json_dumps(data_file_keys, indent=4)))
174 | journal_blocks = []
175 | for key in data_file_keys:
176 | logger.info('Reading file with S3 key {} from bucket: {}.'.format(key, bucket_name))
177 | s3_object = s3_client.get_object(Bucket=bucket_name, Key=key)['Body'].read().decode('utf-8')
178 | blocks = get_journal_blocks(s3_object)
179 | compare_key_with_content_range(key, blocks[0], blocks[len(blocks) - 1])
180 | # `blocks` is also a list of journal blocks, so we need to concatenate them.
181 | journal_blocks.extend(blocks)
182 | return journal_blocks
183 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 |
17 | # Makefile for Sphinx documentation
18 | #
19 |
20 | # You can set these variables from the command line.
21 | SPHINXOPTS =
22 | SPHINXBUILD = sphinx-build
23 | PAPER =
24 | BUILDDIR = build
25 |
26 | # User-friendly check for sphinx-build
27 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
28 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
29 | endif
30 |
31 | # Internal variables.
32 | PAPEROPT_a4 = -D latex_paper_size=a4
33 | PAPEROPT_letter = -D latex_paper_size=letter
34 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
35 | # the i18n builder cannot share the environment and doctrees with the others
36 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
37 |
38 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
39 |
40 | help:
41 | @echo "Please use \`make ' where is one of"
42 | @echo " html to make standalone HTML files"
43 | @echo " dirhtml to make HTML files named index.html in directories"
44 | @echo " singlehtml to make a single large HTML file"
45 | @echo " pickle to make pickle files"
46 | @echo " json to make JSON files"
47 | @echo " htmlhelp to make HTML files and a HTML help project"
48 | @echo " qthelp to make HTML files and a qthelp project"
49 | @echo " devhelp to make HTML files and a Devhelp project"
50 | @echo " epub to make an epub"
51 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
52 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
53 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
54 | @echo " text to make text files"
55 | @echo " man to make manual pages"
56 | @echo " texinfo to make Texinfo files"
57 | @echo " info to make Texinfo files and run them through makeinfo"
58 | @echo " gettext to make PO message catalogs"
59 | @echo " changes to make an overview of all changed/added/deprecated items"
60 | @echo " xml to make Docutils-native XML files"
61 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
62 | @echo " linkcheck to check all external links for integrity"
63 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
64 |
65 | clean:
66 | rm -rf $(BUILDDIR)/*
67 |
68 | html:
69 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
70 | @echo
71 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
72 |
73 | dirhtml:
74 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
75 | @echo
76 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
77 |
78 | singlehtml:
79 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
80 | @echo
81 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
82 |
83 | pickle:
84 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
85 | @echo
86 | @echo "Build finished; now you can process the pickle files."
87 |
88 | json:
89 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
90 | @echo
91 | @echo "Build finished; now you can process the JSON files."
92 |
93 | htmlhelp:
94 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
95 | @echo
96 | @echo "Build finished; now you can run HTML Help Workshop with the" \
97 | ".hhp project file in $(BUILDDIR)/htmlhelp."
98 |
99 | qthelp:
100 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
101 | @echo
102 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
103 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
104 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/test.qhcp"
105 | @echo "To view the help file:"
106 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/test.qhc"
107 |
108 | devhelp:
109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
110 | @echo
111 | @echo "Build finished."
112 | @echo "To view the help file:"
113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/test"
114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/test"
115 | @echo "# devhelp"
116 |
117 | epub:
118 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
119 | @echo
120 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
121 |
122 | latex:
123 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
124 | @echo
125 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
126 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
127 | "(use \`make latexpdf' here to do that automatically)."
128 |
129 | latexpdf:
130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
131 | @echo "Running LaTeX files through pdflatex..."
132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
134 |
135 | latexpdfja:
136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
137 | @echo "Running LaTeX files through platex and dvipdfmx..."
138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
140 |
141 | text:
142 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
143 | @echo
144 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
145 |
146 | man:
147 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
148 | @echo
149 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
150 |
151 | texinfo:
152 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
153 | @echo
154 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
155 | @echo "Run \`make' in that directory to run these through makeinfo" \
156 | "(use \`make info' here to do that automatically)."
157 |
158 | info:
159 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
160 | @echo "Running Texinfo files through makeinfo..."
161 | make -C $(BUILDDIR)/texinfo info
162 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
163 |
164 | gettext:
165 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
166 | @echo
167 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
168 |
169 | changes:
170 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
171 | @echo
172 | @echo "The overview file is in $(BUILDDIR)/changes."
173 |
174 | linkcheck:
175 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
176 | @echo
177 | @echo "Link check complete; look for any errors in the above output " \
178 | "or in $(BUILDDIR)/linkcheck/output.txt."
179 |
180 | doctest:
181 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
182 | @echo "Testing of doctests in the sources finished, look at the " \
183 | "results in $(BUILDDIR)/doctest/output.txt."
184 |
185 | xml:
186 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
187 | @echo
188 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
189 |
190 | pseudoxml:
191 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
192 | @echo
193 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
194 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 |
17 | @ECHO OFF
18 |
19 | REM Command file for Sphinx documentation
20 |
21 | if "%SPHINXBUILD%" == "" (
22 | set SPHINXBUILD=sphinx-build
23 | )
24 | set BUILDDIR=build
25 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
26 | set I18NSPHINXOPTS=%SPHINXOPTS% source
27 | if NOT "%PAPER%" == "" (
28 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
29 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
30 | )
31 |
32 | if "%1" == "" goto help
33 |
34 | if "%1" == "help" (
35 | :help
36 | echo.Please use `make ^` where ^ is one of
37 | echo. html to make standalone HTML files
38 | echo. dirhtml to make HTML files named index.html in directories
39 | echo. singlehtml to make a single large HTML file
40 | echo. pickle to make pickle files
41 | echo. json to make JSON files
42 | echo. htmlhelp to make HTML files and a HTML help project
43 | echo. qthelp to make HTML files and a qthelp project
44 | echo. devhelp to make HTML files and a Devhelp project
45 | echo. epub to make an epub
46 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
47 | echo. text to make text files
48 | echo. man to make manual pages
49 | echo. texinfo to make Texinfo files
50 | echo. gettext to make PO message catalogs
51 | echo. changes to make an overview over all changed/added/deprecated items
52 | echo. xml to make Docutils-native XML files
53 | echo. pseudoxml to make pseudoxml-XML files for display purposes
54 | echo. linkcheck to check all external links for integrity
55 | echo. doctest to run all doctests embedded in the documentation if enabled
56 | goto end
57 | )
58 |
59 | if "%1" == "clean" (
60 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
61 | del /q /s %BUILDDIR%\*
62 | goto end
63 | )
64 |
65 |
66 | %SPHINXBUILD% 2> nul
67 | if errorlevel 9009 (
68 | echo.
69 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
70 | echo.installed, then set the SPHINXBUILD environment variable to point
71 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
72 | echo.may add the Sphinx directory to PATH.
73 | echo.
74 | echo.If you don't have Sphinx installed, grab it from
75 | echo.http://sphinx-doc.org/
76 | exit /b 1
77 | )
78 |
79 | if "%1" == "html" (
80 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
81 | if errorlevel 1 exit /b 1
82 | echo.
83 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
84 | goto end
85 | )
86 |
87 | if "%1" == "dirhtml" (
88 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
89 | if errorlevel 1 exit /b 1
90 | echo.
91 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
92 | goto end
93 | )
94 |
95 | if "%1" == "singlehtml" (
96 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
97 | if errorlevel 1 exit /b 1
98 | echo.
99 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
100 | goto end
101 | )
102 |
103 | if "%1" == "pickle" (
104 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
105 | if errorlevel 1 exit /b 1
106 | echo.
107 | echo.Build finished; now you can process the pickle files.
108 | goto end
109 | )
110 |
111 | if "%1" == "json" (
112 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
113 | if errorlevel 1 exit /b 1
114 | echo.
115 | echo.Build finished; now you can process the JSON files.
116 | goto end
117 | )
118 |
119 | if "%1" == "htmlhelp" (
120 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
121 | if errorlevel 1 exit /b 1
122 | echo.
123 | echo.Build finished; now you can run HTML Help Workshop with the ^
124 | .hhp project file in %BUILDDIR%/htmlhelp.
125 | goto end
126 | )
127 |
128 | if "%1" == "qthelp" (
129 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
130 | if errorlevel 1 exit /b 1
131 | echo.
132 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
133 | .qhcp project file in %BUILDDIR%/qthelp, like this:
134 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\test.qhcp
135 | echo.To view the help file:
136 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\test.ghc
137 | goto end
138 | )
139 |
140 | if "%1" == "devhelp" (
141 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
142 | if errorlevel 1 exit /b 1
143 | echo.
144 | echo.Build finished.
145 | goto end
146 | )
147 |
148 | if "%1" == "epub" (
149 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
150 | if errorlevel 1 exit /b 1
151 | echo.
152 | echo.Build finished. The epub file is in %BUILDDIR%/epub.
153 | goto end
154 | )
155 |
156 | if "%1" == "latex" (
157 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
158 | if errorlevel 1 exit /b 1
159 | echo.
160 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
161 | goto end
162 | )
163 |
164 | if "%1" == "latexpdf" (
165 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
166 | cd %BUILDDIR%/latex
167 | make all-pdf
168 | cd %BUILDDIR%/..
169 | echo.
170 | echo.Build finished; the PDF files are in %BUILDDIR%/latex.
171 | goto end
172 | )
173 |
174 | if "%1" == "latexpdfja" (
175 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
176 | cd %BUILDDIR%/latex
177 | make all-pdf-ja
178 | cd %BUILDDIR%/..
179 | echo.
180 | echo.Build finished; the PDF files are in %BUILDDIR%/latex.
181 | goto end
182 | )
183 |
184 | if "%1" == "text" (
185 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
186 | if errorlevel 1 exit /b 1
187 | echo.
188 | echo.Build finished. The text files are in %BUILDDIR%/text.
189 | goto end
190 | )
191 |
192 | if "%1" == "man" (
193 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
194 | if errorlevel 1 exit /b 1
195 | echo.
196 | echo.Build finished. The manual pages are in %BUILDDIR%/man.
197 | goto end
198 | )
199 |
200 | if "%1" == "texinfo" (
201 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
202 | if errorlevel 1 exit /b 1
203 | echo.
204 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
205 | goto end
206 | )
207 |
208 | if "%1" == "gettext" (
209 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
210 | if errorlevel 1 exit /b 1
211 | echo.
212 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
213 | goto end
214 | )
215 |
216 | if "%1" == "changes" (
217 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
218 | if errorlevel 1 exit /b 1
219 | echo.
220 | echo.The overview file is in %BUILDDIR%/changes.
221 | goto end
222 | )
223 |
224 | if "%1" == "linkcheck" (
225 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
226 | if errorlevel 1 exit /b 1
227 | echo.
228 | echo.Link check complete; look for any errors in the above output ^
229 | or in %BUILDDIR%/linkcheck/output.txt.
230 | goto end
231 | )
232 |
233 | if "%1" == "doctest" (
234 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
235 | if errorlevel 1 exit /b 1
236 | echo.
237 | echo.Testing of doctests in the sources finished, look at the ^
238 | results in %BUILDDIR%/doctest/output.txt.
239 | goto end
240 | )
241 |
242 | if "%1" == "xml" (
243 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
244 | if errorlevel 1 exit /b 1
245 | echo.
246 | echo.Build finished. The XML files are in %BUILDDIR%/xml.
247 | goto end
248 | )
249 |
250 | if "%1" == "pseudoxml" (
251 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
252 | if errorlevel 1 exit /b 1
253 | echo.
254 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
255 | goto end
256 | )
257 |
258 | :end
259 |
--------------------------------------------------------------------------------
/pyqldbsamples/redact_revision.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from logging import basicConfig, getLogger, INFO
20 | from time import sleep
21 |
22 | from pyqldbsamples.add_secondary_owner import get_document_ids, SampleData
23 | from pyqldbsamples.connect_to_ledger import create_qldb_driver
24 | from pyqldbsamples.constants import Constants
25 | from pyqldbsamples.model.sample_data import print_ion
26 | from pyqldbsamples.transfer_vehicle_ownership import validate_and_update_registration
27 |
28 | logger = getLogger(__name__)
29 | basicConfig(level=INFO)
30 |
31 |
32 | def get_table_id(transaction_executor, table_name):
33 | """
34 | Get the tableId of a table with the given table name.
35 |
36 | :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
37 | :param transaction_executor: An Executor object allowing for execution of statements
38 | within a transaction.
39 |
40 | :type table_name: :py:class:`amazon.ion.simple_types.IonPyText`
41 | :param table_name: The table name for which we'll get the tableId.
42 |
43 | :rtype: :py:class:`amazon.ion.simple_types.IonPyText`
44 | :return: The tableId for the provided table name.
45 | """
46 | logger.info("Getting the tableId for table with name: {}".format(table_name))
47 | query = "SELECT VALUE tableId FROM information_schema.user_tables WHERE name = '{}'".format(table_name)
48 | results = list(transaction_executor.execute_statement(query))
49 | logger.info("Results list: {}".format(results))
50 | if len(results) == 0:
51 | raise Exception("Unable to find table with name {}".format(table_name))
52 | return results[0]
53 |
54 |
55 | def get_historic_registration_by_owner_id(transaction_executor, registration_document_id, owner_document_id):
56 | """
57 | Get the historic revision of a vehicle registration with the given ownerId.
58 |
59 | :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
60 | :param transaction_executor: An Executor object allowing for execution of statements
61 | within a transaction.
62 |
63 | :type registration_document_id: :py:class:`amazon.ion.simple_types.IonPyText`
64 | :param registration_document_id: The QLDB DocumentId of the vehicle registration document.
65 |
66 | :type owner_document_id: :py:class:`amazon.ion.simple_types.IonPyText`
67 | :param owner_document_id: The QLDB DocumentId of the vehicle owner.
68 |
69 | :rtype :py:class:`amazon.ion.simple_types.IonPySymbol`
70 | :return An Ion Struct returned by QLDB that represents a document revision.
71 | """
72 | logger.info(
73 | "Querying the 'VehicleRegistration' table's history for a registration with documentId: {} and owner: {}"
74 | .format(registration_document_id, owner_document_id)
75 | )
76 | query = "SELECT * FROM history({}) AS h WHERE h.metadata.id = '{}' AND h.data.Owners.PrimaryOwner.PersonId = '{}'" \
77 | .format(Constants.VEHICLE_REGISTRATION_TABLE_NAME, registration_document_id, owner_document_id)
78 | results = list(transaction_executor.execute_statement(query))
79 | if len(results) == 0:
80 | raise Exception(
81 | "Unable to find historic registration with documentId: {} and ownerId: {}"
82 | .format(registration_document_id, owner_document_id)
83 | )
84 | elif len(results) > 1:
85 | raise Exception(
86 | "Found more than 1 historic registrations with documentId: {} and ownerId: {}"
87 | .format(registration_document_id, owner_document_id)
88 | )
89 | result = results[0]
90 | print_ion(result)
91 | return result
92 |
93 |
94 | def get_historic_registration_by_version_number(transaction_executor, registration_document_id, version_number):
95 | """
96 | Get the historic revision of a vehicle registration with the given document version.
97 |
98 | :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
99 | :param transaction_executor: An Executor object allowing for execution of statements
100 | within a transaction.
101 |
102 | :type registration_document_id: :py:class:`amazon.ion.simple_types.IonPyText`
103 | :param registration_document_id: The QLDB DocumentId of the vehicle registration document.
104 |
105 | :type version_number: :py:class:`amazon.ion.simple_types.IonPyInt`
106 | :param version_number: The version of the vehicle registration document to query the history for.
107 |
108 | :rtype :py:class:`amazon.ion.simple_types.IonPySymbol`
109 | :return An Ion Struct returned by QLDB that represents a document revision.
110 | """
111 | logger.info(
112 | "Querying the 'VehicleRegistration' table's history for a registration with documentId: {} and version: {}"
113 | .format(registration_document_id, version_number)
114 | )
115 | query = "SELECT * FROM history({}) AS h WHERE h.metadata.id = '{}' AND h.metadata.version = {}" \
116 | .format(Constants.VEHICLE_REGISTRATION_TABLE_NAME, registration_document_id, version_number)
117 | results = list(transaction_executor.execute_statement(query))
118 | if len(results) == 0:
119 | raise Exception(
120 | "Unable to find historic registration with documentId: {} and version: {}"
121 | .format(registration_document_id, version_number)
122 | )
123 | result = results[0]
124 | print_ion(result)
125 | return result
126 |
127 |
128 | def redact_previous_registration(transaction_executor, vin, previous_owner_gov_id):
129 | """
130 | Redact a historic revision of a vehicle registration.
131 |
132 | :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
133 | :param transaction_executor: An Executor object allowing for execution of statements
134 | within a transaction.
135 |
136 | :type vin: :py:class:`amazon.ion.simple_types.IonPyText`
137 | :param vin: The VIN specified in the vehicle registration.
138 |
139 | :type previous_owner_gov_id: :py:class:`amazon.ion.simple_types.IonPyText`
140 | :param previous_owner_gov_id: The OwnerId on the previous revision of the
141 | vehicle registration that is going to be redacted.
142 |
143 | :rtype :py:class:`amazon.ion.simple_types.IonPySymbol`
144 | :return An Ion Struct returned as a response to a redaction request.
145 | """
146 | table_id = get_table_id(transaction_executor, Constants.VEHICLE_REGISTRATION_TABLE_NAME)
147 | registration_document_id = \
148 | get_document_ids(transaction_executor, Constants.VEHICLE_REGISTRATION_TABLE_NAME, 'VIN', vin)[0]
149 | previous_owner_document_id = \
150 | get_document_ids(transaction_executor, Constants.PERSON_TABLE_NAME, 'GovId', previous_owner_gov_id)[0]
151 | historic_revision_block_address = get_historic_registration_by_owner_id(
152 | transaction_executor, registration_document_id, previous_owner_document_id
153 | )['blockAddress']
154 |
155 | logger.info("Redacting the revision at blockAddress: {} with tableId: {} and documentId: {}"
156 | .format(historic_revision_block_address, table_id, registration_document_id))
157 | redact_query = "EXEC redact_revision ?, '{}', '{}'".format(table_id, registration_document_id)
158 | redact_request = next(transaction_executor.execute_statement(redact_query, historic_revision_block_address))
159 | print_ion(redact_request)
160 | return redact_request
161 |
162 |
163 | def wait_till_revision_redacted(driver, redact_request):
164 | """
165 | Wait until a revision is redacted by checking the history of the document.
166 |
167 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
168 | :param driver: An instance of the QldbDriver class.
169 |
170 | :type redact_request: :py:class:`amazon.ion.simple_types.IonPySymbol`
171 | :param redact_request: An Ion Struct returned as a response to a redaction request.
172 | """
173 | is_redacted = False
174 | while not is_redacted:
175 | revision = driver.execute_lambda(lambda executor: get_historic_registration_by_version_number(
176 | executor, redact_request['documentId'], redact_request['version']
177 | ))
178 | if 'data' not in revision and 'dataHash' in revision:
179 | is_redacted = True
180 | logger.info("Revision was successfully redacted.")
181 | else:
182 | logger.info("Revision is not yet redacted. Waiting for sometime...")
183 | sleep(10)
184 |
185 |
186 | def main(ledger_name=Constants.LEDGER_NAME):
187 | """
188 | Redact a historic revision of a vehicle registration document.
189 | """
190 | vehicle_vin = SampleData.VEHICLE[2]['VIN']
191 | previous_owner = SampleData.PERSON[2]['GovId']
192 | new_owner = SampleData.PERSON[3]['GovId']
193 |
194 | try:
195 | with create_qldb_driver(ledger_name) as driver:
196 | validate_and_update_registration(driver, vehicle_vin, previous_owner, new_owner)
197 | redact_request = driver.execute_lambda(
198 | lambda executor: redact_previous_registration(executor, vehicle_vin, previous_owner))
199 | wait_till_revision_redacted(driver, redact_request)
200 | except Exception as e:
201 | logger.exception('Error redacting VehicleRegistration.')
202 | raise e
203 |
204 |
205 | if __name__ == '__main__':
206 | main()
207 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 |
17 | #!/usr/bin/env python3
18 | # -*- coding: utf-8 -*-
19 | #
20 | # AmazonQLDB Sample App documentation build configuration file, created by
21 | # sphinx-quickstart on Mon Oct 7 18:23:00 2019.
22 | #
23 | # This file is execfile()d with the current directory set to its
24 | # containing dir.
25 | #
26 | # Note that not all possible configuration values are present in this
27 | # autogenerated file.
28 | #
29 | # All configuration values have a default; values that are commented out
30 | # serve to show the default.
31 |
32 |
33 | import os
34 |
35 | import pyqldbsamples
36 |
37 | # -- General configuration -----------------------------------------------------
38 |
39 | # If your documentation needs a minimal Sphinx version, state it here.
40 | #needs_sphinx = '1.0'
41 |
42 | # Add any Sphinx extension module names here, as strings. They can be extensions
43 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
44 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
45 |
46 | # Add any paths that contain templates here, relative to this directory.
47 | templates_path = ['_templates']
48 |
49 | # The suffix of source filenames.
50 | source_suffix = '.rst'
51 |
52 | # The encoding of source files.
53 | #source_encoding = 'utf-8-sig'
54 |
55 | # The master toctree document.
56 | master_doc = 'index'
57 |
58 | # General information about the project.
59 | project = 'AmazonQLDB Python Sample App Docs'
60 | copyright = '2019, Amazon Web Services, Inc'
61 |
62 | # The version info for the project you're documenting, acts as replacement for
63 | # |version| and |release|, also used in various other places throughout the
64 | # built documents.
65 | #
66 | # The short X.Y version.
67 | version = pyqldbsamples.__version__
68 | # The full version, including alpha/beta/rc tags.
69 | release = pyqldbsamples.__version__
70 |
71 | # The language for content autogenerated by Sphinx. Refer to documentation
72 | # for a list of supported languages.
73 | #language = None
74 |
75 | # There are two options for replacing |today|: either, you set today to some
76 | # non-false value, then it is used:
77 | #today = ''
78 | # Else, today_fmt is used as the format for a strftime call.
79 | #today_fmt = '%B %d, %Y'
80 |
81 | # List of patterns, relative to source directory, that match files and
82 | # directories to ignore when looking for source files.
83 | exclude_patterns = []
84 |
85 | # The reST default role (used for this markup: `text`) to use for all documents.
86 | #default_role = None
87 |
88 | # If true, '()' will be appended to :func: etc. cross-reference text.
89 | #add_function_parentheses = True
90 |
91 | # If true, the current module name will be prepended to all description
92 | # unit titles (such as .. function::).
93 | # add_module_names = True
94 |
95 | # If true, sectionauthor and moduleauthor directives will be shown in the
96 | # output. They are ignored by default.
97 | #show_authors = False
98 |
99 | # The name of the Pygments (syntax highlighting) style to use.
100 | pygments_style = 'sphinx'
101 |
102 | # A list of ignored prefixes for module index sorting.
103 | #modindex_common_prefix = []
104 |
105 |
106 | import guzzle_sphinx_theme
107 |
108 | extensions.append("guzzle_sphinx_theme")
109 | html_translator_class = 'guzzle_sphinx_theme.HTMLTranslator'
110 | html_theme_path = guzzle_sphinx_theme.html_theme_path()
111 | html_theme = 'guzzle_sphinx_theme'
112 | # Guzzle theme options (see theme.conf for more information)
113 |
114 | html_theme_options = {
115 | # hack to add tracking
116 | "google_analytics_account": os.getenv('TRACKING', False),
117 | "base_url": "http://docs.aws.amazon.com/aws-sdk-php/guide/latest/"
118 | }
119 |
120 | # -- Options for HTML output ---------------------------------------------------
121 |
122 | # The theme to use for HTML and HTML Help pages. See the documentation for
123 | # a list of builtin themes.
124 | #html_theme = 'default'
125 |
126 | # Theme options are theme-specific and customize the look and feel of a theme
127 | # further. For a list of options available for each theme, see the
128 | # documentation.
129 | #html_theme_options = {}
130 |
131 | # Add any paths that contain custom themes here, relative to this directory.
132 | #html_theme_path = []
133 |
134 | # The name for this set of Sphinx documents. If None, it defaults to
135 | # " v documentation".
136 | #html_title = None
137 |
138 | # A shorter title for the navigation bar. Default is the same as html_title.
139 | #html_short_title = None
140 |
141 | # The name of an image file (relative to this directory) to place at the top
142 | # of the sidebar.
143 | #html_logo = None
144 |
145 | # The name of an image file (within the static path) to use as favicon of the
146 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
147 | # pixels large.
148 | #html_favicon = None
149 |
150 | # Add any paths that contain custom static files (such as style sheets) here,
151 | # relative to this directory. They are copied after the builtin static files,
152 | # so a file named "default.css" will overwrite the builtin "default.css".
153 | html_static_path = ['_static']
154 |
155 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
156 | # using the given strftime format.
157 | #html_last_updated_fmt = '%b %d, %Y'
158 |
159 | # If true, SmartyPants will be used to convert quotes and dashes to
160 | # typographically correct entities.
161 | #html_use_smartypants = True
162 |
163 | # Custom sidebar templates, maps document names to template names.
164 | html_show_sourcelink = False
165 | html_sidebars = {
166 | '**': ['logo-text.html',
167 | 'globaltoc.html',
168 | 'searchbox.html']
169 | }
170 |
171 | # Additional templates that should be rendered to pages, maps page names to
172 | # template names.
173 | #html_additional_pages = {}
174 |
175 | # If false, no module index is generated.
176 | #html_domain_indices = True
177 |
178 | # If false, no index is generated.
179 | #html_use_index = True
180 |
181 | # If true, the index is split into individual pages for each letter.
182 | #html_split_index = False
183 |
184 | # If true, links to the reST sources are added to the pages.
185 | # html_show_sourcelink = True
186 |
187 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
188 | #html_show_sphinx = True
189 |
190 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
191 | #html_show_copyright = True
192 |
193 | # If true, an OpenSearch description file will be output, and all pages will
194 | # contain a tag referring to it. The value of this option must be the
195 | # base URL from which the finished HTML is served.
196 | #html_use_opensearch = ''
197 |
198 | # This is the file name suffix for HTML files (e.g. ".xhtml").
199 | #html_file_suffix = None
200 |
201 | # Output file base name for HTML help builder.
202 | htmlhelp_basename = 'AmazonQLDBPythonSampleAppDocs'
203 |
204 |
205 | # -- Options for LaTeX output --------------------------------------------------
206 |
207 | latex_elements = {
208 | # The paper size ('letterpaper' or 'a4paper').
209 | #'papersize': 'letterpaper',
210 |
211 | # The font size ('10pt', '11pt' or '12pt').
212 | #'pointsize': '10pt',
213 |
214 | # Additional stuff for the LaTeX preamble.
215 | #'preamble': '',
216 | }
217 |
218 | # Grouping the document tree into LaTeX files. List of tuples
219 | # (source start file, target name, title, author, documentclass [howto/manual]).
220 | latex_documents = [
221 | ('index', 'AmazonQLDBPythonSampleApp.tex', 'AmazonQLDB Python Sample App Documentation',
222 | 'Amazon.com, Inc.', 'manual'),
223 | ]
224 |
225 | # The name of an image file (relative to this directory) to place at the top of
226 | # the title page.
227 | #latex_logo = None
228 |
229 | # For "manual" documents, if this is true, then toplevel headings are parts,
230 | # not chapters.
231 | #latex_use_parts = False
232 |
233 | # If true, show page references after internal links.
234 | #latex_show_pagerefs = False
235 |
236 | # If true, show URL addresses after external links.
237 | #latex_show_urls = False
238 |
239 | # Documents to append as an appendix to all manuals.
240 | #latex_appendices = []
241 |
242 | # If false, no module index is generated.
243 | #latex_domain_indices = True
244 |
245 |
246 | # -- Options for manual page output --------------------------------------------
247 |
248 | # One entry per manual page. List of tuples
249 | # (source start file, name, description, authors, manual section).
250 | man_pages = [
251 | ('index', 'AmazonQLDBPythonSampleApp', 'AmazonQLDB Python Sample App Documentation',
252 | ['Amazon.com, Inc.'], 1)
253 | ]
254 |
255 | # If true, show URL addresses after external links.
256 | #man_show_urls = False
257 |
258 |
259 | # -- Options for Texinfo output ------------------------------------------------
260 |
261 | # Grouping the document tree into Texinfo files. List of tuples
262 | # (source start file, target name, title, author,
263 | # dir menu entry, description, category)
264 | texinfo_documents = [
265 | ('index', 'AmazonQLDBPythonSampleApp', 'AmazonQLDB Python Sample App Documentation',
266 | 'Amazon.com, Inc.', 'AmazonQLDB', 'One line description of project.',
267 | 'Miscellaneous'),
268 | ]
269 |
270 | # Documents to append as an appendix to all manuals.
271 | #texinfo_appendices = []
272 |
273 | # If false, no module index is generated.
274 | #texinfo_domain_indices = True
275 |
276 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
277 | #texinfo_show_urls = 'footnote'
278 |
279 | autoclass_content = 'both'
--------------------------------------------------------------------------------
/pyqldbsamples/insert_ion_types.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this
5 | # software and associated documentation files (the "Software"), to deal in the Software
6 | # without restriction, including without limitation the rights to use, copy, modify,
7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | # permit persons to whom the Software is furnished to do so.
9 | #
10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 | #
17 | # This code expects that you have AWS credentials setup per:
18 | # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
19 | from datetime import datetime
20 | from decimal import Decimal
21 | from logging import basicConfig, getLogger, INFO
22 |
23 | from amazon.ion.simple_types import IonPyBool, IonPyBytes, IonPyDecimal, IonPyDict, IonPyFloat, IonPyInt, IonPyList, \
24 | IonPyNull, IonPySymbol, IonPyText, IonPyTimestamp
25 | from amazon.ion.simpleion import loads
26 | from amazon.ion.symbols import SymbolToken
27 | from amazon.ion.core import IonType
28 |
29 | from pyqldbsamples.create_table import create_table
30 | from pyqldbsamples.constants import Constants
31 | from pyqldbsamples.insert_document import insert_documents
32 | from pyqldbsamples.model.sample_data import convert_object_to_ion
33 | from pyqldbsamples.connect_to_ledger import create_qldb_driver
34 |
35 | logger = getLogger(__name__)
36 | basicConfig(level=INFO)
37 |
38 | TABLE_NAME = 'IonTypes'
39 |
40 |
41 | def update_record_and_verify_type(driver, parameter, ion_object, ion_type):
42 | """
43 | Update a record in the database table. Then query the value of the record and verify correct ion type saved.
44 |
45 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
46 | :param driver: An instance of the QldbDriver class.
47 |
48 | :type parameter: :py:class:`amazon.ion.simple_types.IonPyValue`
49 | :param parameter: The Ion value or Python native type that is convertible to Ion for filling in parameters of the
50 | statement.
51 |
52 | :type ion_object: :py:obj:`IonPyBool`/:py:obj:`IonPyBytes`/:py:obj:`IonPyDecimal`/:py:obj:`IonPyDict`
53 | /:py:obj:`IonPyFloat`/:py:obj:`IonPyInt`/:py:obj:`IonPyList`/:py:obj:`IonPyNull`
54 | /:py:obj:`IonPySymbol`/:py:obj:`IonPyText`/:py:obj:`IonPyTimestamp`
55 | :param ion_object: The Ion object to verify against.
56 |
57 | :type ion_type: :py:class:`amazon.ion.core.IonType`
58 | :param ion_type: The Ion type to verify against.
59 |
60 | :raises TypeError: When queried value is not an instance of Ion type.
61 | """
62 | update_query = 'UPDATE {} SET Name = ?'.format(TABLE_NAME)
63 | driver.execute_lambda(lambda executor: executor.execute_statement(update_query, parameter))
64 | logger.info('Updated record.')
65 |
66 | search_query = 'SELECT VALUE Name FROM {}'.format(TABLE_NAME)
67 | cursor = driver.execute_lambda(lambda executor: executor.execute_statement(search_query))
68 |
69 | for c in cursor:
70 | if not isinstance(c, ion_object):
71 | raise TypeError('The queried value is not an instance of {}'.format(ion_object.__name__))
72 |
73 | if c.ion_type is not ion_type:
74 | raise TypeError('The queried value type does not match {}'.format(ion_type))
75 |
76 | logger.info("Successfully verified value is instance of '{}' with type '{}'.".format(ion_object.__name__, ion_type))
77 | return cursor
78 |
79 |
80 | def delete_table(driver, table_name):
81 | """
82 | Delete a table.
83 |
84 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
85 | :param driver: An instance of the QldbDriver class.
86 |
87 | :type table_name: str
88 | :param table_name: Name of the table to delete.
89 |
90 | :rtype: int
91 | :return: The number of changes to the database.
92 | """
93 | logger.info("Deleting '{}' table...".format(table_name))
94 | cursor = driver.execute_lambda(lambda executor: executor.execute_statement('DROP TABLE {}'.format(table_name)))
95 | logger.info("'{}' table successfully deleted.".format(table_name))
96 | return len(list(cursor))
97 |
98 |
99 | def insert_and_verify_ion_types(driver):
100 | """
101 | Insert all the supported Ion types and Python values that are convertible to Ion into a ledger and verify that they
102 | are stored and can be retrieved properly, retaining their original properties.
103 |
104 | :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
105 | :param driver: A QLDB Driver object.
106 | """
107 | python_bytes = str.encode('hello')
108 | python_bool = True
109 | python_float = float('0.2')
110 | python_decimal = Decimal('0.1')
111 | python_string = "string"
112 | python_int = 1
113 | python_null = None
114 | python_datetime = datetime(2016, 12, 20, 5, 23, 43)
115 | python_list = [1, 2]
116 | python_dict = {"brand": "Ford"}
117 |
118 | ion_clob = convert_object_to_ion(loads('{{"This is a CLOB of text."}}'))
119 | ion_blob = convert_object_to_ion(python_bytes)
120 | ion_bool = convert_object_to_ion(python_bool)
121 | ion_decimal = convert_object_to_ion(python_decimal)
122 | ion_float = convert_object_to_ion(python_float)
123 | ion_int = convert_object_to_ion(python_int)
124 | ion_list = convert_object_to_ion(python_list)
125 | ion_null = convert_object_to_ion(python_null)
126 | ion_sexp = convert_object_to_ion(loads('(cons 1 2)'))
127 | ion_string = convert_object_to_ion(python_string)
128 | ion_struct = convert_object_to_ion(python_dict)
129 | ion_symbol = convert_object_to_ion(SymbolToken(text='abc', sid=123))
130 | ion_timestamp = convert_object_to_ion(python_datetime)
131 |
132 | ion_null_clob = convert_object_to_ion(loads('null.clob'))
133 | ion_null_blob = convert_object_to_ion(loads('null.blob'))
134 | ion_null_bool = convert_object_to_ion(loads('null.bool'))
135 | ion_null_decimal = convert_object_to_ion(loads('null.decimal'))
136 | ion_null_float = convert_object_to_ion(loads('null.float'))
137 | ion_null_int = convert_object_to_ion(loads('null.int'))
138 | ion_null_list = convert_object_to_ion(loads('null.list'))
139 | ion_null_sexp = convert_object_to_ion(loads('null.sexp'))
140 | ion_null_string = convert_object_to_ion(loads('null.string'))
141 | ion_null_struct = convert_object_to_ion(loads('null.struct'))
142 | ion_null_symbol = convert_object_to_ion(loads('null.symbol'))
143 | ion_null_timestamp = convert_object_to_ion(loads('null.timestamp'))
144 |
145 | create_table(driver, TABLE_NAME)
146 | insert_documents(driver, TABLE_NAME, [{'Name': 'val'}])
147 | update_record_and_verify_type(driver, python_bytes, IonPyBytes, IonType.BLOB)
148 | update_record_and_verify_type(driver, python_bool, IonPyBool, IonType.BOOL)
149 | update_record_and_verify_type(driver, python_float, IonPyFloat, IonType.FLOAT)
150 | update_record_and_verify_type(driver, python_decimal, IonPyDecimal, IonType.DECIMAL)
151 | update_record_and_verify_type(driver, python_string, IonPyText, IonType.STRING)
152 | update_record_and_verify_type(driver, python_int, IonPyInt, IonType.INT)
153 | update_record_and_verify_type(driver, python_null, IonPyNull, IonType.NULL)
154 | update_record_and_verify_type(driver, python_datetime, IonPyTimestamp, IonType.TIMESTAMP)
155 | update_record_and_verify_type(driver, python_list, IonPyList, IonType.LIST)
156 | update_record_and_verify_type(driver, python_dict, IonPyDict, IonType.STRUCT)
157 | update_record_and_verify_type(driver, ion_clob, IonPyBytes, IonType.CLOB)
158 | update_record_and_verify_type(driver, ion_blob, IonPyBytes, IonType.BLOB)
159 | update_record_and_verify_type(driver, ion_bool, IonPyBool, IonType.BOOL)
160 | update_record_and_verify_type(driver, ion_decimal, IonPyDecimal, IonType.DECIMAL)
161 | update_record_and_verify_type(driver, ion_float, IonPyFloat, IonType.FLOAT)
162 | update_record_and_verify_type(driver, ion_int, IonPyInt, IonType.INT)
163 | update_record_and_verify_type(driver, ion_list, IonPyList, IonType.LIST)
164 | update_record_and_verify_type(driver, ion_null, IonPyNull, IonType.NULL)
165 | update_record_and_verify_type(driver, ion_sexp, IonPyList, IonType.SEXP)
166 | update_record_and_verify_type(driver, ion_string, IonPyText, IonType.STRING)
167 | update_record_and_verify_type(driver, ion_struct, IonPyDict, IonType.STRUCT)
168 | update_record_and_verify_type(driver, ion_symbol, IonPySymbol, IonType.SYMBOL)
169 | update_record_and_verify_type(driver, ion_timestamp, IonPyTimestamp, IonType.TIMESTAMP)
170 | update_record_and_verify_type(driver, ion_null_clob, IonPyNull, IonType.CLOB)
171 | update_record_and_verify_type(driver, ion_null_blob, IonPyNull, IonType.BLOB)
172 | update_record_and_verify_type(driver, ion_null_bool, IonPyNull, IonType.BOOL)
173 | update_record_and_verify_type(driver, ion_null_decimal, IonPyNull, IonType.DECIMAL)
174 | update_record_and_verify_type(driver, ion_null_float, IonPyNull, IonType.FLOAT)
175 | update_record_and_verify_type(driver, ion_null_int, IonPyNull, IonType.INT)
176 | update_record_and_verify_type(driver, ion_null_list, IonPyNull, IonType.LIST)
177 | update_record_and_verify_type(driver, ion_null_sexp, IonPyNull, IonType.SEXP)
178 | update_record_and_verify_type(driver, ion_null_string, IonPyNull, IonType.STRING)
179 | update_record_and_verify_type(driver, ion_null_struct, IonPyNull, IonType.STRUCT)
180 | update_record_and_verify_type(driver, ion_null_symbol, IonPyNull, IonType.SYMBOL)
181 | update_record_and_verify_type(driver, ion_null_timestamp, IonPyNull, IonType.TIMESTAMP)
182 | delete_table(driver, TABLE_NAME)
183 |
184 |
185 | def main(ledger_name=Constants.LEDGER_NAME):
186 | """
187 | Insert all the supported Ion types and Python values that are convertible to Ion into a ledger and verify that they
188 | are stored and can be retrieved properly, retaining their original properties.
189 | """
190 | try:
191 | with create_qldb_driver(ledger_name) as driver:
192 | insert_and_verify_ion_types(driver)
193 | except Exception as e:
194 | logger.exception('Error updating and validating Ion types.')
195 | raise e
196 |
197 |
198 | if __name__ == '__main__':
199 | main()
200 |
--------------------------------------------------------------------------------