├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── THIRD-PARTY ├── ecs-vpc ├── .gitignore ├── README.md ├── app.py ├── cdk.json ├── ecs_vpc │ ├── __init__.py │ └── ecs_vpc_stack.py ├── requirements.txt ├── setup.py └── source.bat ├── fhir-hl7-transform ├── cdk-infra │ ├── .gitignore │ ├── README.md │ ├── app.py │ ├── cdk.json │ ├── fhir_to_hl7v2_transform │ │ ├── __init__.py │ │ └── transform_stack.py │ ├── requirements.txt │ ├── setup.py │ └── source.bat ├── container │ ├── Dockerfile │ ├── application │ │ └── hl7_sender.py │ └── requirements.txt └── lambda │ ├── .gitignore │ ├── exclude.lst │ ├── lib │ ├── fhir_resource_reader.py │ ├── fhir_resource_writer.py │ ├── fhir_to_hl7.py │ ├── hl7_message_builder.py │ ├── hl7_message_parser.py │ └── hl7_to_fhir.py │ ├── requirements.txt │ └── transform.py ├── resources ├── architecture.png └── patient.json └── test-hl7-server ├── cdk-infra ├── .gitignore ├── README.md ├── app.py ├── cdk.json ├── cdk_infra │ ├── __init__.py │ └── test_hl7_server_stack.py ├── requirements.txt ├── setup.py └── source.bat └── container ├── Dockerfile ├── application └── hl7_listener.py └── requirements.txt /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fhir-works-on-aws-hl7v2-transform 2 | 3 | This AWS sample project demonstrates implementation of an Integration Transform designed to extend third-party integration capabilities of [FHIR Works on AWS framework](https://aws.amazon.com/blogs/opensource/using-open-source-fhir-apis-with-fhir-works-on-aws/). 4 | 5 | ## Architecture 6 | 7 | This sample architecture consists of multiple AWS serverless services. The endpoint is hosted using API Gateway backed by a Lambda function. We chose to use SQS to pass messages from transform function to Fargate container implementing HL7v2 client (sender). 8 | 9 | We also implemented Test HL7 Server using NLB, Fargate, and S3. 10 | 11 | In production environment, you would need to establish secure encrypted connection from your VPC to the network where your HL7 endpoint is hosted. For illustration purposes, we show encrypted connection from your VPC to your corporate data center using VPN tunnel. You should consult your account team for prescriptive guidance regarding establishing secure and reliable network connection between your on-premises network and VPC. 12 | 13 | ![Architecture](resources/architecture.png) 14 | 15 | ## Implementation Notes 16 | 17 | ### FHIR POST and PUT Interactions 18 | 19 | FHIR resource write interactions (POST and PUT) produce HL7v2 messages that then will be forwarded to the third-party system via its HL7v2 interface endpoint. In our sample, we send HL7v2 messages via TCP/IP socket connection to a Test HL7 Server which stores them on S3. 20 | 21 | ### FHIR GET Interaction 22 | 23 | Implementation of this interaction depends on third-party system integration capabilities. In our case, we implemented it by taking advantage of the Test HL7 Server that we deploy in the account. This test server stores HL7v2 messages as objects in S3 bucket. Other possible implementation may require direct interaction with operational data store (using JDBC or ODBC connections), HTTP API, or HL7v2 query messages. 24 | 25 | ## Deployment 26 | 27 | ### Pre-requisites 28 | 29 | - [AWS Cloud Development Kit (CDK)](https://aws.amazon.com/cdk/). Please install CDK on your workstation using [instructions in AWS documentation](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) 30 | - [AWS Command Line Interface (CLI)](https://aws.amazon.com/cli/) 31 | - [AWS configuration and credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html). We suggest that you create a named profile (referred to as`` in command line examples below) for your IAM user that you can use to pass to `cdk` and `aws` command line utilities. 32 | 33 | #### Bootstrap CDK (Optional) 34 | 35 | ``` 36 | cdk [--profile ] bootstrap aws://{aws_account_id}/{region} 37 | ``` 38 | 39 | where `{aws_account_id}` represents your AWS Account ID. 40 | 41 | ### (Optional) VPC Stack 42 | 43 | You can use an existing VPC to deploy Integration Transform and (optional) Test HL7 Server stacks. You can also create new VPC using `ecs-vpc` CDK stack included with this sample. You can also specify optional context variable `cidr` to control IP address space configuration. 44 | 45 | ``` 46 | cd ${REPOSITORY_ROOT}/ecs-vpc 47 | python3 -m venv .env 48 | source .env/bin/activate 49 | pip3 install -r requirements.txt 50 | cdk [--profile ] deploy --context cidr="10.10.0.0/16" 51 | ``` 52 | 53 | Please note VPC ID that will be displayed after VPC stack is deployed: 54 | 55 | ``` 56 | ecs-vpc.TransformVpc = vpc-example-id 57 | ``` 58 | 59 | ### (Optional) HL7 Server Stack 60 | 61 | ``` 62 | 63 | cd ${REPOSITORY_ROOT}/test-hl7-server/cdk-infra 64 | python3 -m venv .env 65 | source .env/bin/activate 66 | pip3 install -r requirements.txt 67 | cdk [--profile ] deploy --context vpc-id="vpc-example-id" --context server-port=2575 68 | 69 | ``` 70 | 71 | Copy and save the following outputs that you will need to pass as inputs to the Integration Transform stack 72 | 73 | ``` 74 | test-hl7-server-stack.Hl7TestServerServiceLoadBalancerDNSHEX_ID = .elb..amazonaws.com 75 | test-hl7-server-stack.TestHl7ServerFQDN = .elb..amazonaws.com 76 | test-hl7-server-stack.TestHl7ServerS3 = test-hl7-server-stack-hl7testserveroutputbucket 77 | ``` 78 | 79 | ### Transform Stack 80 | 81 | Before deploying Integration Transform stack, you will need to obtain FHIR Works on AWS resource router lambda execution role (`FHIR_SERVICE_LAMBDA_ROLE_ARN: FhirServiceLambdaRoleArn`), which is one of outputs of the FHIR Works on AWS deployment stack. Please refer to [this section of the documentation](https://github.com/awslabs/fhir-works-on-aws-deployment/blob/api/INSTALL.md#aws-service-deployment) describing FHIR Works on AWS deployment. You can use `serverless info --verbose --aws-profile --stage --region ` to produce stack output post deployment. 82 | 83 | You will pass lambda execution role to the Integration Transform deployment process as CDK context variable `resource-router-lambda-role`. 84 | 85 | Other context variables: 86 | 87 | `vpc-id` : ID of VPC where you would like to launch ECS Fargate cluster component of this architecture 88 | 89 | `hl7-server-name`: FQDN (or IP address) of the HL7v2 endpoint that will be listening for connection from HL7v2 sender component. If you deployed HL7 test server, you can use value of output parameter `test-hl7-server-stack.TestHl7ServerFQDN` 90 | 91 | `hl7-port`: TCP port that HL7v2 server will be listening on 92 | 93 | `test-server-output-bucket-name`: if you deploy optional Test HL7 Server stack, you can find this parameter in the stack outputs (`test-hl7-server-stack.TestHl7ServerS3`) 94 | 95 | ``` 96 | cd ${REPOSITORY_ROOT}/fhir-hl7-transform/cdk-infra 97 | python3 -m venv .env 98 | source .env/bin/activate 99 | pip3 install -r requirements.txt 100 | cdk [--profile ] deploy --context vpc-id="vpc-example-id" --context resource-router-lambda-role="lambda-execution-role-arn" --context hl7-server-name="hl7server.example.com" --context hl7-port="2575" --context test-server-output-bucket-name="" 101 | ``` 102 | 103 | #### Outputs 104 | 105 | CDK deployment will generate several outputs, you will need to take note of two of them: 106 | 107 | ``` 108 | fhir-to-hl7v2-transform.TransformApiRegion = us-west-2 109 | fhir-to-hl7v2-transform.TransformApiRootUrl = https://.execute-api.us-west-2.amazonaws.com/prod/ 110 | ``` 111 | 112 | These values will be used to create AWS System Manager parameters on FHIR Works on AWS. FHIR Works on AWS will make API requests to the URL and region specified in these parameters. This allows FHIR Works on AWS to integrate with this Integration Transform. 113 | FHIR Works on AWS Integration Transform parameters are: `/fhir-service/integration-transform//url` and `/fhir-service/integration-transform//awsRegion`, where STAGE is the stage name that you used when deploying FHIR Works on AWS. For more information please refer to this [link](https://github.com/awslabs/fhir-works-on-aws-deployment/blob/api/INSTALL.md#store-integration-transform-info-in-aws-system-manager-parameter-store). 114 | 115 | These outputs can also be viewed in AWS Console by navigating to CloudFormation service page in the region where the stack was deployed, selecting appropriate stack, and clicking on Outputs tab. 116 | 117 | ## Testing 118 | 119 | You can follow testing steps outlined in [FHIR Works on AWS documentation](https://github.com/awslabs/fhir-works-on-aws-deployment/blob/api/README.md#usage-instructions). This Integration Transform support CREATE, READ, and UPDATE (not DELETE) interactions on Patient resource. An example Patient resource can be found [here](resources/patient.json). 120 | 121 | ## Security 122 | 123 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 124 | 125 | ## License 126 | 127 | This library is licensed under the MIT-0 License. See the LICENSE file. 128 | -------------------------------------------------------------------------------- /THIRD-PARTY: -------------------------------------------------------------------------------- 1 | ** aws-cdk; version 1.58.0 -- https://github.com/aws/aws-cdk 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | © 2020 GitHub, Inc. 205 | 206 | * For aws-cdk see also this required NOTICE: 207 | AWS Cloud Development Kit (AWS CDK) 208 | Copyright 2018-2018 Amazon.com, Inc. or its affiliates. All Rights 209 | Reserved. 210 | 211 | ------ 212 | 213 | ** boto3; version 1.15.22 -- https://github.com/boto/boto3/ 214 | 215 | Apache License 216 | 217 | Version 2.0, January 2004 218 | 219 | http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND 220 | DISTRIBUTION 221 | 222 | 1. Definitions. 223 | 224 | "License" shall mean the terms and conditions for use, reproduction, and 225 | distribution as defined by Sections 1 through 9 of this document. 226 | 227 | "Licensor" shall mean the copyright owner or entity authorized by the 228 | copyright owner that is granting the License. 229 | 230 | "Legal Entity" shall mean the union of the acting entity and all other 231 | entities that control, are controlled by, or are under common control 232 | with that entity. For the purposes of this definition, "control" means 233 | (i) the power, direct or indirect, to cause the direction or management 234 | of such entity, whether by contract or otherwise, or (ii) ownership of 235 | fifty percent (50%) or more of the outstanding shares, or (iii) 236 | beneficial ownership of such entity. 237 | 238 | "You" (or "Your") shall mean an individual or Legal Entity exercising 239 | permissions granted by this License. 240 | 241 | "Source" form shall mean the preferred form for making modifications, 242 | including but not limited to software source code, documentation source, 243 | and configuration files. 244 | 245 | "Object" form shall mean any form resulting from mechanical 246 | transformation or translation of a Source form, including but not limited 247 | to compiled object code, generated documentation, and conversions to 248 | other media types. 249 | 250 | "Work" shall mean the work of authorship, whether in Source or Object 251 | form, made available under the License, as indicated by a copyright 252 | notice that is included in or attached to the work (an example is 253 | provided in the Appendix below). 254 | 255 | "Derivative Works" shall mean any work, whether in Source or Object form, 256 | that is based on (or derived from) the Work and for which the editorial 257 | revisions, annotations, elaborations, or other modifications represent, 258 | as a whole, an original work of authorship. For the purposes of this 259 | License, Derivative Works shall not include works that remain separable 260 | from, or merely link (or bind by name) to the interfaces of, the Work and 261 | Derivative Works thereof. 262 | 263 | "Contribution" shall mean any work of authorship, including the original 264 | version of the Work and any modifications or additions to that Work or 265 | Derivative Works thereof, that is intentionally submitted to Licensor for 266 | inclusion in the Work by the copyright owner or by an individual or Legal 267 | Entity authorized to submit on behalf of the copyright owner. For the 268 | purposes of this definition, "submitted" means any form of electronic, 269 | verbal, or written communication sent to the Licensor or its 270 | representatives, including but not limited to communication on electronic 271 | mailing lists, source code control systems, and issue tracking systems 272 | that are managed by, or on behalf of, the Licensor for the purpose of 273 | discussing and improving the Work, but excluding communication that is 274 | conspicuously marked or otherwise designated in writing by the copyright 275 | owner as "Not a Contribution." 276 | 277 | "Contributor" shall mean Licensor and any individual or Legal Entity on 278 | behalf of whom a Contribution has been received by Licensor and 279 | subsequently incorporated within the Work. 280 | 281 | 2. Grant of Copyright License. Subject to the terms and conditions of this 282 | License, each Contributor hereby grants to You a perpetual, worldwide, 283 | non-exclusive, no-charge, royalty-free, irrevocable copyright license to 284 | reproduce, prepare Derivative Works of, publicly display, publicly perform, 285 | sublicense, and distribute the Work and such Derivative Works in Source or 286 | Object form. 287 | 288 | 3. Grant of Patent License. Subject to the terms and conditions of this 289 | License, each Contributor hereby grants to You a perpetual, worldwide, 290 | non-exclusive, no-charge, royalty-free, irrevocable (except as stated in 291 | this section) patent license to make, have made, use, offer to sell, sell, 292 | import, and otherwise transfer the Work, where such license applies only to 293 | those patent claims licensable by such Contributor that are necessarily 294 | infringed by their Contribution(s) alone or by combination of their 295 | Contribution(s) with the Work to which such Contribution(s) was submitted. 296 | If You institute patent litigation against any entity (including a 297 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 298 | Contribution incorporated within the Work constitutes direct or contributory 299 | patent infringement, then any patent licenses granted to You under this 300 | License for that Work shall terminate as of the date such litigation is 301 | filed. 302 | 303 | 4. Redistribution. You may reproduce and distribute copies of the Work or 304 | Derivative Works thereof in any medium, with or without modifications, and 305 | in Source or Object form, provided that You meet the following conditions: 306 | 307 | (a) You must give any other recipients of the Work or Derivative Works a 308 | copy of this License; and 309 | 310 | (b) You must cause any modified files to carry prominent notices stating 311 | that You changed the files; and 312 | 313 | (c) You must retain, in the Source form of any Derivative Works that You 314 | distribute, all copyright, patent, trademark, and attribution notices 315 | from the Source form of the Work, excluding those notices that do not 316 | pertain to any part of the Derivative Works; and 317 | 318 | (d) If the Work includes a "NOTICE" text file as part of its 319 | distribution, then any Derivative Works that You distribute must include 320 | a readable copy of the attribution notices contained within such NOTICE 321 | file, excluding those notices that do not pertain to any part of the 322 | Derivative Works, in at least one of the following places: within a 323 | NOTICE text file distributed as part of the Derivative Works; within the 324 | Source form or documentation, if provided along with the Derivative 325 | Works; or, within a display generated by the Derivative Works, if and 326 | wherever such third-party notices normally appear. The contents of the 327 | NOTICE file are for informational purposes only and do not modify the 328 | License. You may add Your own attribution notices within Derivative Works 329 | that You distribute, alongside or as an addendum to the NOTICE text from 330 | the Work, provided that such additional attribution notices cannot be 331 | construed as modifying the License. 332 | 333 | You may add Your own copyright statement to Your modifications and may 334 | provide additional or different license terms and conditions for use, 335 | reproduction, or distribution of Your modifications, or for any such 336 | Derivative Works as a whole, provided Your use, reproduction, and 337 | distribution of the Work otherwise complies with the conditions stated in 338 | this License. 339 | 340 | 5. Submission of Contributions. Unless You explicitly state otherwise, any 341 | Contribution intentionally submitted for inclusion in the Work by You to the 342 | Licensor shall be under the terms and conditions of this License, without 343 | any additional terms or conditions. Notwithstanding the above, nothing 344 | herein shall supersede or modify the terms of any separate license agreement 345 | you may have executed with Licensor regarding such Contributions. 346 | 347 | 6. Trademarks. This License does not grant permission to use the trade 348 | names, trademarks, service marks, or product names of the Licensor, except 349 | as required for reasonable and customary use in describing the origin of the 350 | Work and reproducing the content of the NOTICE file. 351 | 352 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in 353 | writing, Licensor provides the Work (and each Contributor provides its 354 | Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 355 | KIND, either express or implied, including, without limitation, any 356 | warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or 357 | FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining 358 | the appropriateness of using or redistributing the Work and assume any risks 359 | associated with Your exercise of permissions under this License. 360 | 361 | 8. Limitation of Liability. In no event and under no legal theory, whether 362 | in tort (including negligence), contract, or otherwise, unless required by 363 | applicable law (such as deliberate and grossly negligent acts) or agreed to 364 | in writing, shall any Contributor be liable to You for damages, including 365 | any direct, indirect, special, incidental, or consequential damages of any 366 | character arising as a result of this License or out of the use or inability 367 | to use the Work (including but not limited to damages for loss of goodwill, 368 | work stoppage, computer failure or malfunction, or any and all other 369 | commercial damages or losses), even if such Contributor has been advised of 370 | the possibility of such damages. 371 | 372 | 9. Accepting Warranty or Additional Liability. While redistributing the Work 373 | or Derivative Works thereof, You may choose to offer, and charge a fee for, 374 | acceptance of support, warranty, indemnity, or other liability obligations 375 | and/or rights consistent with this License. However, in accepting such 376 | obligations, You may act only on Your own behalf and on Your sole 377 | responsibility, not on behalf of any other Contributor, and only if You 378 | agree to indemnify, defend, and hold each Contributor harmless for any 379 | liability incurred by, or claims asserted against, such Contributor by 380 | reason of your accepting any such warranty or additional liability. END OF 381 | TERMS AND CONDITIONS 382 | 383 | APPENDIX: How to apply the Apache License to your work. 384 | 385 | To apply the Apache License to your work, attach the following boilerplate 386 | notice, with the fields enclosed by brackets "[]" replaced with your own 387 | identifying information. (Don't include the brackets!) The text should be 388 | enclosed in the appropriate comment syntax for the file format. We also 389 | recommend that a file or class name and description of purpose be included on 390 | the same "printed page" as the copyright notice for easier identification 391 | within third-party archives. 392 | 393 | Copyright [yyyy] [name of copyright owner] 394 | 395 | Licensed under the Apache License, Version 2.0 (the "License"); 396 | 397 | you may not use this file except in compliance with the License. 398 | 399 | You may obtain a copy of the License at 400 | 401 | http://www.apache.org/licenses/LICENSE-2.0 402 | 403 | Unless required by applicable law or agreed to in writing, software 404 | 405 | distributed under the License is distributed on an "AS IS" BASIS, 406 | 407 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 408 | 409 | See the License for the specific language governing permissions and 410 | 411 | limitations under the License. 412 | 413 | * For boto3 see also this required NOTICE: 414 | Copyright 2013-2019 Amazon.com, Inc. or its affiliates. All Rights 415 | Reserved. 416 | 417 | ------ 418 | 419 | ** hl7apy; version 1.3.3 -- https://github.com/crs4/hl7apy 420 | Copyright (c) 2012-2018, CRS4 421 | 422 | ** twisted; version 20.3.0 -- https://github.com/twisted/twisted 423 | Copyright (c) 2001-2020 424 | Allen Short 425 | Amber Hawkie Brown 426 | Andrew Bennetts 427 | Andy Gayton 428 | Antoine Pitrou 429 | Apple Computer, Inc. 430 | Ashwini Oruganti 431 | Benjamin Bruheim 432 | Bob Ippolito 433 | Canonical Limited 434 | Christopher Armstrong 435 | Ciena Corporation 436 | David Reid 437 | Divmod Inc. 438 | Donovan Preston 439 | Eric Mangold 440 | Eyal Lotem 441 | Google Inc. 442 | Hybrid Logic Ltd. 443 | Hynek Schlawack 444 | Itamar Turner-Trauring 445 | James Knight 446 | Jason A. Mobarak 447 | Jean-Paul Calderone 448 | Jessica McKellar 449 | Jonathan D. Simms 450 | Jonathan Jacobs 451 | Jonathan Lange 452 | Julian Berman 453 | Jürgen Hermann 454 | Kevin Horn 455 | Kevin Turner 456 | Laurens Van Houtven 457 | Mary Gardiner 458 | Massachusetts Institute of Technology 459 | Matthew Lefkowitz 460 | Moshe Zadka 461 | Paul Swartz 462 | Pavel Pergamenshchik 463 | Rackspace, US Inc. 464 | Ralph Meijer 465 | Richard Wall 466 | Sean Riley 467 | Software Freedom Conservancy 468 | Tavendo GmbH 469 | Thijs Triemstra 470 | Thomas Grainger 471 | Thomas Herve 472 | Timothy Allen 473 | Tom Most 474 | Tom Prince 475 | Travis B. Hartwell 476 | 477 | and others that have contributed code to the public domain. 478 | 479 | Permission is hereby granted, free of charge, to any person obtaining a copy of 480 | this software and associated documentation files (the "Software"), to deal in 481 | the Software without restriction, including without limitation the rights to 482 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 483 | of 484 | the Software, and to permit persons to whom the Software is furnished to do so, 485 | subject to the following conditions: 486 | 487 | The above copyright notice and this permission notice shall be included in all 488 | copies or substantial portions of the Software. 489 | 490 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 491 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 492 | FITNESS 493 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 494 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 495 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 496 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 497 | 498 | ------ 499 | 500 | ** python-hl7; version 0.4.0 -- 501 | https://python-hl7.readthedocs.io/en/latest/api.html 502 | Copyright (C) 2009-2020 John Paulett (john -at- paulett.org) 503 | All rights reserved. 504 | ** txHL7; version 0.1.0 -- https://txhl7.readthedocs.io/en/latest/index.html 505 | Copyright (C) 2011-2014 John Paulett (john -at- paulett.org) 506 | All rights reserved. 507 | 508 | Redistribution and use in source and binary forms, with or without 509 | modification, are permitted provided that the following conditions 510 | are met: 511 | 512 | 1. Redistributions of source code must retain the above copyright 513 | notice, this list of conditions and the following disclaimer. 514 | 2. Redistributions in binary form must reproduce the above copyright 515 | notice, this list of conditions and the following disclaimer in 516 | the documentation and/or other materials provided with the 517 | distribution. 518 | 3. The name of the author may not be used to endorse or promote 519 | products derived from this software without specific prior 520 | written permission. 521 | 522 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS 523 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 524 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 525 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 526 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 527 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 528 | GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 529 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 530 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 531 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 532 | IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 533 | -------------------------------------------------------------------------------- /ecs-vpc/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | package-lock.json 3 | __pycache__ 4 | .pytest_cache 5 | .env 6 | *.egg-info 7 | 8 | # CDK asset staging directory 9 | .cdk.staging 10 | cdk.out 11 | -------------------------------------------------------------------------------- /ecs-vpc/README.md: -------------------------------------------------------------------------------- 1 | ### VPC for ECS on Fargate containers 2 | The purpose of this optional CDK app is to create Virtual Private network in two availability zones with one private and one public subnet. This VPC will be used to facilitate ECS on Fargate container networking. -------------------------------------------------------------------------------- /ecs-vpc/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | 5 | from aws_cdk import core 6 | 7 | from ecs_vpc.ecs_vpc_stack import EcsVpcStack 8 | 9 | 10 | app = core.App() 11 | EcsVpcStack(app, "ecs-vpc") 12 | 13 | app.synth() 14 | -------------------------------------------------------------------------------- /ecs-vpc/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 app.py", 3 | "context": { 4 | "@aws-cdk/core:enableStackNameDuplicates": "true", 5 | "aws-cdk:enableDiffNoFail": "true", 6 | "@aws-cdk/core:stackRelativeExports": "true" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ecs-vpc/ecs_vpc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/fhir-hl7v2-integration-transform/a34d9c406b031aff362ee18bea36aacddbb74e32/ecs-vpc/ecs_vpc/__init__.py -------------------------------------------------------------------------------- /ecs-vpc/ecs_vpc/ecs_vpc_stack.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | from aws_cdk import core 5 | from aws_cdk import aws_ec2 as ec2 6 | 7 | 8 | class EcsVpcStack(core.Stack): 9 | def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: 10 | super().__init__(scope, id, **kwargs) 11 | 12 | cidr = self.node.try_get_context("cidr") 13 | if not cidr: 14 | cidr = "10.10.0.0/16" 15 | 16 | ecs_vpc = ec2.Vpc( 17 | self, 18 | "FhirTransformVpc", 19 | cidr=cidr, 20 | max_azs=2, 21 | ) 22 | 23 | core.CfnOutput( 24 | self, 25 | "TransformVpc", 26 | value=ecs_vpc.vpc_id, 27 | export_name="TransformVpc", 28 | ) 29 | -------------------------------------------------------------------------------- /ecs-vpc/requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | -------------------------------------------------------------------------------- /ecs-vpc/setup.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import setuptools 5 | 6 | with open("README.md") as fp: 7 | long_description = fp.read() 8 | 9 | 10 | setuptools.setup( 11 | name="ecs_vpc", 12 | version="0.0.1", 13 | description="CDK Python app implementing VPC", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | author="author", 17 | package_dir={"": "ecs_vpc"}, 18 | packages=setuptools.find_packages(where="ecs_vpc"), 19 | install_requires=[ 20 | "aws-cdk.core==1.68.0", 21 | "aws-cdk.aws_ec2==1.68.0", 22 | ], 23 | python_requires=">=3.6", 24 | classifiers=[ 25 | "Development Status :: 4 - Beta", 26 | "Intended Audience :: Developers", 27 | "License :: OSI Approved :: Apache Software License", 28 | "Programming Language :: JavaScript", 29 | "Programming Language :: Python :: 3 :: Only", 30 | "Programming Language :: Python :: 3.6", 31 | "Programming Language :: Python :: 3.7", 32 | "Programming Language :: Python :: 3.8", 33 | "Topic :: Software Development :: Code Generators", 34 | "Topic :: Utilities", 35 | "Typing :: Typed", 36 | ], 37 | ) 38 | -------------------------------------------------------------------------------- /ecs-vpc/source.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem The sole purpose of this script is to make the command 4 | rem 5 | rem source .env/bin/activate 6 | rem 7 | rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. 8 | rem On Windows, this command just runs this batch file (the argument is ignored). 9 | rem 10 | rem Now we don't need to document a Windows command for activating a virtualenv. 11 | 12 | echo Executing .env\Scripts\activate.bat for you 13 | .env\Scripts\activate.bat 14 | -------------------------------------------------------------------------------- /fhir-hl7-transform/cdk-infra/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | package-lock.json 3 | __pycache__ 4 | .pytest_cache 5 | .env 6 | *.egg-info 7 | 8 | # CDK asset staging directory 9 | .cdk.staging 10 | cdk.out 11 | template.yaml 12 | cdk.context.json 13 | environment.json 14 | -------------------------------------------------------------------------------- /fhir-hl7-transform/cdk-infra/README.md: -------------------------------------------------------------------------------- 1 | ### FHIR Works on AWS Integration Transform 2 | This CDK app implements an exemplar Integration Transform demonstrating how to integrate FHIR Works on AWS with third-party system. -------------------------------------------------------------------------------- /fhir-hl7-transform/cdk-infra/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | 5 | import os 6 | 7 | from aws_cdk import core 8 | 9 | from fhir_to_hl7v2_transform.transform_stack import FhirToHl7V2TransformStack 10 | 11 | app = core.App() 12 | 13 | transform_stack = FhirToHl7V2TransformStack( 14 | app, 15 | "fhir-to-hl7v2-transform", 16 | env=core.Environment( 17 | account=os.environ["CDK_DEFAULT_ACCOUNT"], 18 | region=os.environ["CDK_DEFAULT_REGION"], 19 | ), 20 | ) 21 | 22 | 23 | app.synth() 24 | -------------------------------------------------------------------------------- /fhir-hl7-transform/cdk-infra/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 app.py", 3 | "context": { 4 | "@aws-cdk/core:enableStackNameDuplicates": "true", 5 | "aws-cdk:enableDiffNoFail": "true", 6 | "@aws-cdk/core:stackRelativeExports": "true" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /fhir-hl7-transform/cdk-infra/fhir_to_hl7v2_transform/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/fhir-hl7v2-integration-transform/a34d9c406b031aff362ee18bea36aacddbb74e32/fhir-hl7-transform/cdk-infra/fhir_to_hl7v2_transform/__init__.py -------------------------------------------------------------------------------- /fhir-hl7-transform/cdk-infra/fhir_to_hl7v2_transform/transform_stack.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | from os import path 5 | 6 | from aws_cdk import aws_apigateway as apigw 7 | from aws_cdk import aws_ec2 as ec2 8 | from aws_cdk import aws_ecs as ecs 9 | from aws_cdk import aws_ecs_patterns as ecs_patterns 10 | from aws_cdk import aws_iam as iam 11 | from aws_cdk import aws_lambda as lambda_ 12 | from aws_cdk import aws_logs as logs 13 | from aws_cdk import aws_s3 as s3 14 | from aws_cdk import aws_sqs as sqs 15 | from aws_cdk import aws_ssm as ssm 16 | from aws_cdk import core 17 | from aws_solutions_constructs import aws_apigateway_lambda as apigw_lambda 18 | 19 | dirname = path.dirname(__file__) 20 | 21 | COMPONENT_PREFIX = "FhirToHl7v2" 22 | COMPONENT_PREFIX_DASHES = "fhir-to-hl7v2" 23 | 24 | 25 | class FhirToHl7V2TransformStack(core.Stack): 26 | def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: 27 | super().__init__(scope, id, **kwargs) 28 | 29 | # API Gateway needs to have resource policy granting FHIR Works on AWS lambda 30 | # execute permissions. Lambda function ARN will be passed during deployment as CDK context variable 31 | # FHIR Works lambda will need to have policy attached to its execution role 32 | # allowing it to invoke API 33 | # From --context resource-router-lambda-role="arn:aws:iam::123456789012:role/rolename" 34 | imported_resource_router_lambda_role = self.node.try_get_context( 35 | "resource-router-lambda-role" 36 | ) 37 | # Amazon ECS on AWS Fargate container implementing connection manager 38 | # will be launched into a VPC that needs to have private and public subnets 39 | # and NAT gateway or instance 40 | # From --context vpc-id="vpc-123456" 41 | vpc_id = self.node.try_get_context("vpc-id") 42 | 43 | # The following parameters specify name of the HL7 server 44 | # that will be receiving transformed HL7v2 messages and TCP port 45 | # that it will be listening on 46 | # From --context hl7-server-name="hl7.example.com" 47 | # From --context hl7-port="2575" 48 | hl7_server_name = self.node.try_get_context("hl7-server-name") 49 | hl7_port = self.node.try_get_context("hl7-port") 50 | 51 | # In this proof of concept source of data for read interactions 52 | # is S3 bucket where mock HL7 server stores processed HL7 messages 53 | # From --context test-server-output-bucket-name="DOC-EXAMPLE-BUCKET" 54 | test_server_output_bucket_name = self.node.try_get_context( 55 | "test-server-output-bucket-name" 56 | ) 57 | 58 | # SQS queue 59 | # Custom transform lambda communicates with Connectivity Manager using this SQS queue 60 | queue = sqs.Queue( 61 | self, f"{COMPONENT_PREFIX}Queue", encryption=sqs.QueueEncryption.KMS_MANAGED 62 | ) 63 | 64 | # S3 Bucket to retrieve HL7v2 messages in proof of concept deployment 65 | test_server_output_bucket = s3.Bucket.from_bucket_name( 66 | self, f"{COMPONENT_PREFIX}OutputBucket", test_server_output_bucket_name 67 | ) 68 | 69 | # Transform Lambda 70 | # Reference implementation of Custom Transform component of Transform Execution Environment 71 | 72 | transform_lambda = lambda_.Function( 73 | self, 74 | f"{COMPONENT_PREFIX}TransformLambda", 75 | handler="transform.handler", 76 | runtime=lambda_.Runtime.PYTHON_3_8, 77 | code=lambda_.Code.from_asset( 78 | path.join(dirname, "../../lambda"), 79 | bundling={ 80 | "image": lambda_.Runtime.PYTHON_3_8.bundling_docker_image, 81 | "command": [ 82 | "bash", 83 | "-c", 84 | " && ".join( 85 | [ 86 | "pip install --no-cache-dir -r requirements.txt -t /asset-output", 87 | "(tar -c --exclude-from=exclude.lst -f - .)|(cd /asset-output; tar -xf -)", 88 | ] 89 | ), 90 | ], 91 | }, 92 | ), 93 | timeout=core.Duration.seconds(60), 94 | environment=dict( 95 | SQS_QUEUE=queue.queue_url, 96 | # The following parameter is optional 97 | S3_BUCKET_NAME=test_server_output_bucket_name, 98 | ), 99 | ) 100 | queue.grant_send_messages(transform_lambda) 101 | 102 | # API Gateway with Lambda construct (using https://aws.amazon.com/solutions/constructs/patterns) 103 | # Reference implementation of Custom Transform component of Transform Execution Environment 104 | 105 | api_lambda = apigw_lambda.ApiGatewayToLambda( 106 | self, 107 | "ApiGw", 108 | existing_lambda_obj=transform_lambda, 109 | api_gateway_props=apigw.LambdaRestApiProps( 110 | handler=transform_lambda, 111 | proxy=False, 112 | rest_api_name=f"{COMPONENT_PREFIX_DASHES}-api", 113 | endpoint_export_name=f"{COMPONENT_PREFIX}ApiEndPoint", 114 | description=f"{COMPONENT_PREFIX} APIGW with Transform Lambda (FHIR to HL7v2)", 115 | default_method_options=apigw.MethodOptions( 116 | authorization_type=apigw.AuthorizationType.IAM, 117 | ), 118 | policy=iam.PolicyDocument( 119 | statements=[ 120 | iam.PolicyStatement( 121 | actions=["execute-api:Invoke"], 122 | effect=iam.Effect.ALLOW, 123 | principals=[ 124 | iam.ArnPrincipal(imported_resource_router_lambda_role), 125 | ], 126 | resources=["execute-api:/*/*/*"], 127 | ) 128 | ] 129 | ), 130 | ), 131 | ) 132 | rest_api = api_lambda.api_gateway 133 | persistence = rest_api.root.add_resource("persistence") 134 | resource_type = persistence.add_resource("{resource_type}") 135 | resource_type.add_method("POST") 136 | resource_id = resource_type.add_resource("{id}") 137 | resource_id.add_method("GET") 138 | resource_id.add_method("PUT") 139 | resource_id.add_method("DELETE") 140 | 141 | # ECS Fargate Container (HL7v2 sender) 142 | # This container implements Connectivity Manager component 143 | # of Transform Execution Environment 144 | 145 | vpc = ec2.Vpc.from_lookup(self, "DefaultVpc", vpc_id=vpc_id) 146 | 147 | cluster = ecs.Cluster(self, f"{COMPONENT_PREFIX}Cluster", vpc=vpc) 148 | 149 | ecs_patterns.QueueProcessingFargateService( 150 | self, 151 | f"{COMPONENT_PREFIX}Service", 152 | cluster=cluster, 153 | image=ecs.ContainerImage.from_asset(path.join(dirname, "../../container")), 154 | queue=queue, 155 | desired_task_count=1, 156 | log_driver=ecs.LogDriver.aws_logs( 157 | stream_prefix=f"{COMPONENT_PREFIX}HL7Client", 158 | log_retention=logs.RetentionDays.ONE_DAY, 159 | ), 160 | environment=dict( 161 | SERVER_NAME=hl7_server_name, 162 | PORT_NUMBER=hl7_port, 163 | ), 164 | ) 165 | 166 | # The following permission grants are needed to support 167 | # read interactions with integration transform 168 | test_server_output_bucket.grant_read(transform_lambda) 169 | 170 | transform_lambda.add_to_role_policy( 171 | iam.PolicyStatement( 172 | actions=["s3:ListBucket"], 173 | effect=iam.Effect.ALLOW, 174 | resources=[test_server_output_bucket.bucket_arn], 175 | ) 176 | ) 177 | transform_lambda.add_to_role_policy( 178 | iam.PolicyStatement( 179 | actions=["s3:GetObject"], 180 | effect=iam.Effect.ALLOW, 181 | resources=[test_server_output_bucket.arn_for_objects("*")], 182 | ) 183 | ) 184 | 185 | # CloudFormation Stack outputs 186 | # The following outputs needed to configure FHIR Works on AWS API interface 187 | core.CfnOutput( 188 | self, 189 | "TransformApiRootUrl", 190 | value=rest_api.url, 191 | export_name="TransformApiRootUrl", 192 | ) 193 | core.CfnOutput( 194 | self, 195 | "TransformApiRegion", 196 | value=self.region, 197 | export_name="TransformApiRegion", 198 | ) 199 | core.CfnOutput( 200 | self, 201 | "TransformApiAccountId", 202 | value=self.account, 203 | export_name="TransformApiAccountId", 204 | ) 205 | -------------------------------------------------------------------------------- /fhir-hl7-transform/cdk-infra/requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | -------------------------------------------------------------------------------- /fhir-hl7-transform/cdk-infra/setup.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import setuptools 5 | 6 | 7 | with open("README.md") as fp: 8 | long_description = fp.read() 9 | 10 | 11 | setuptools.setup( 12 | name="fhir_to_hl7v2_transform", 13 | version="0.0.2", 14 | description="CDK App to deploy FHIR to HL7v2 transform proof of concept architecture", 15 | long_description=long_description, 16 | long_description_content_type="text/markdown", 17 | author="Bakha Nurzhanov ", 18 | package_dir={"": "fhir_to_hl7v2_transform"}, 19 | packages=setuptools.find_packages(where="fhir_to_hl7v2_transform"), 20 | install_requires=[ 21 | "aws-cdk.core==1.69.0", 22 | "aws-cdk.aws-lambda==1.69.0", 23 | "aws-cdk.aws_ecs==1.69.0", 24 | "aws-cdk.aws_ecs_patterns==1.69.0", 25 | "aws_solutions_constructs.aws_apigateway_lambda==1.69.0", 26 | ], 27 | python_requires=">=3.6", 28 | classifiers=[ 29 | "Development Status :: 4 - Beta", 30 | "Intended Audience :: Developers", 31 | "License :: OSI Approved :: Apache Software License", 32 | "Programming Language :: JavaScript", 33 | "Programming Language :: Python :: 3 :: Only", 34 | "Programming Language :: Python :: 3.6", 35 | "Programming Language :: Python :: 3.7", 36 | "Programming Language :: Python :: 3.8", 37 | "Topic :: Software Development :: Code Generators", 38 | "Topic :: Utilities", 39 | "Typing :: Typed", 40 | ], 41 | ) 42 | -------------------------------------------------------------------------------- /fhir-hl7-transform/cdk-infra/source.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem The sole purpose of this script is to make the command 4 | rem 5 | rem source .env/bin/activate 6 | rem 7 | rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. 8 | rem On Windows, this command just runs this batch file (the argument is ignored). 9 | rem 10 | rem Now we don't need to document a Windows command for activating a virtualenv. 11 | 12 | echo Executing .env\Scripts\activate.bat for you 13 | .env\Scripts\activate.bat 14 | -------------------------------------------------------------------------------- /fhir-hl7-transform/container/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # Container image to run microservice implementing 4 | # HL7v2 client (sender) 5 | FROM python:3.8 6 | 7 | WORKDIR /application 8 | 9 | COPY requirements.txt . 10 | 11 | RUN pip install --no-cache-dir -r requirements.txt 12 | 13 | COPY application/ . 14 | 15 | CMD [ "python", "./hl7_sender.py" ] 16 | -------------------------------------------------------------------------------- /fhir-hl7-transform/container/application/hl7_sender.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | import logging 4 | import os 5 | from signal import SIGINT, SIGTERM, signal 6 | 7 | import boto3 8 | from hl7.client import MLLPClient 9 | 10 | logger = logging.getLogger() 11 | logger.setLevel(logging.INFO) 12 | 13 | 14 | class SignalHandler: 15 | def __init__(self): 16 | self.received_signal = False 17 | signal(SIGINT, self._signal_handler) 18 | signal(SIGTERM, self._signal_handler) 19 | 20 | def _signal_handler(self, signal, frame): 21 | logger.info(f"handling signal {signal}, exiting gracefully") 22 | self.received_signal = True 23 | 24 | 25 | sqs = boto3.resource("sqs") 26 | queue = sqs.get_queue_by_name(QueueName=os.environ["QUEUE_NAME"]) 27 | port_number = int(os.environ.get("PORT_NUMBER", 2575)) 28 | server_name = os.environ.get("SERVER_NAME", "localhost") 29 | 30 | 31 | def process_message(client, body): 32 | client.send_message(body) 33 | 34 | 35 | def main(): 36 | signal_handler = SignalHandler() 37 | while True: 38 | try: 39 | with MLLPClient(server_name, port_number) as client: 40 | print(f"Connecting to {server_name} on {port_number}", flush=True) 41 | while not signal_handler.received_signal: 42 | messages = queue.receive_messages( 43 | AttributeNames=["All"], 44 | MaxNumberOfMessages=10, 45 | WaitTimeSeconds=10, 46 | ) 47 | for message in messages: 48 | try: 49 | print("Processing message...", flush=True) 50 | process_message(client, message.body) 51 | # BrokenPipeError: [Errno 32] Broken pipe 52 | # ConnectionResetError: [Errno 104] Connection reset by peer 53 | except ConnectionResetError as exc: 54 | client.close() 55 | raise exc 56 | except ConnectionAbortedError as exc: 57 | client.close() 58 | raise exc 59 | except Exception as exc: 60 | client.close() 61 | logger.exception( 62 | f"Exception: {repr(exc)} Connection: {server_name}:{port_number}", 63 | exc_info=exc, 64 | ) 65 | # raise RuntimeError( 66 | # f"Parameters: {server_name}:{port_number}" 67 | # ) from exc 68 | raise exc 69 | else: 70 | message.delete() 71 | except ConnectionResetError as exc: 72 | print(f"Reconnecting due to {exc}") 73 | continue 74 | except ConnectionAbortedError as exc: 75 | print(f"Reconnecting due to {exc}") 76 | continue 77 | except BrokenPipeError as exc: 78 | print(f"Reconnecting due to {exc}") 79 | continue 80 | except TimeoutError as exc: 81 | print(f"Reconnecting due to {exc}") 82 | continue 83 | 84 | 85 | if __name__ == "__main__": 86 | main() 87 | -------------------------------------------------------------------------------- /fhir-hl7-transform/container/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | hl7apy 3 | hl7 4 | -------------------------------------------------------------------------------- /fhir-hl7-transform/lambda/.gitignore: -------------------------------------------------------------------------------- 1 | hl7apy* 2 | -------------------------------------------------------------------------------- /fhir-hl7-transform/lambda/exclude.lst: -------------------------------------------------------------------------------- 1 | exclude.lst 2 | .history 3 | .gitignore 4 | .env 5 | -------------------------------------------------------------------------------- /fhir-hl7-transform/lambda/lib/fhir_resource_reader.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import boto3 5 | from lib.hl7_to_fhir import Hl7v2ToFhirConverter 6 | 7 | 8 | class FhirResourceReader(object): 9 | """ 10 | Class representing FHIR resource reader 11 | """ 12 | 13 | def __init__(self, s3_bucket_name: str, path_parameters: dict) -> None: 14 | self._s3_bucket_name = s3_bucket_name 15 | self._path_parameters = path_parameters 16 | self._resource_type = self._path_parameters.get("resource_type") 17 | self._resource_id = self._path_parameters.get("id") 18 | 19 | def _get_hl7_from_s3(self) -> str: 20 | s3 = boto3.resource("s3") 21 | obj_key = f"{self._resource_type}/{self._resource_id}" 22 | hl7obj = s3.Object(self._s3_bucket_name, obj_key) 23 | return hl7obj.get()["Body"].read().decode("utf-8") 24 | 25 | def read(self) -> str: 26 | self._hl7msg = self._get_hl7_from_s3() 27 | 28 | return Hl7v2ToFhirConverter( 29 | self._hl7msg, self._resource_type, self._resource_id 30 | ).transform() 31 | -------------------------------------------------------------------------------- /fhir-hl7-transform/lambda/lib/fhir_resource_writer.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | from typing import Tuple 5 | from uuid import uuid4 6 | 7 | from lib.fhir_to_hl7 import FhirToHL7v2Converter 8 | from lib.hl7_to_fhir import Hl7v2ToFhirConverter 9 | 10 | 11 | class FhirResourceWriter: 12 | """ 13 | Class representing FHIR resource writer 14 | """ 15 | 16 | def __init__(self, payload: dict, path_parameters: dict = None) -> None: 17 | self._fhir_resource = payload 18 | self._path_parameters = path_parameters 19 | 20 | def write(self) -> (Tuple[str, str]): 21 | fhir_resource = self._set_resource_id() 22 | resource_type = self._get_resource_type() 23 | resource_id = fhir_resource.get("id") 24 | message = FhirToHL7v2Converter(fhir_resource, resource_type).transform() 25 | 26 | fhir_resource = Hl7v2ToFhirConverter( 27 | message, resource_type, resource_id 28 | ).transform() 29 | 30 | return (message, fhir_resource) 31 | 32 | def _get_resource_type(self) -> str: 33 | return self._fhir_resource.get("resourceType") 34 | 35 | def _set_resource_id(self) -> dict: 36 | fhir_resource = self._fhir_resource 37 | if self._path_parameters: 38 | resource_id = self._path_parameters["id"] 39 | else: 40 | resource_id = str(uuid4()) 41 | fhir_resource["id"] = resource_id 42 | if fhir_resource.get("identifier") is None: 43 | fhir_resource["identifier"] = list() 44 | for identifier in fhir_resource["identifier"]: 45 | if identifier.get("value", "") == resource_id: 46 | break 47 | else: 48 | fhir_resource["identifier"].append( 49 | { 50 | "value": resource_id, 51 | "type": { 52 | "coding": [ 53 | { 54 | "code": "FW", 55 | }, 56 | ], 57 | }, 58 | "assigner": { 59 | "display": "FHIR Works on AWS Integration Transform", 60 | }, 61 | } 62 | ) 63 | 64 | return fhir_resource 65 | -------------------------------------------------------------------------------- /fhir-hl7-transform/lambda/lib/fhir_to_hl7.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | from lib.hl7_message_builder import create_adt_message 5 | 6 | 7 | class FhirToHL7v2Converter(object): 8 | def __init__(self, fhir_resource: dict, resource_type: str) -> None: 9 | self._fhir_resource = fhir_resource 10 | self._resource_type = resource_type 11 | 12 | def transform(self) -> str: 13 | if self._resource_type == "Patient": 14 | return create_adt_message(self._fhir_resource) 15 | -------------------------------------------------------------------------------- /fhir-hl7-transform/lambda/lib/hl7_message_builder.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | from datetime import datetime 4 | from uuid import uuid4 5 | 6 | from hl7apy import set_default_version as hl7_set_version 7 | from hl7apy.core import Field, Message, Segment 8 | 9 | 10 | def _create_hl7_message(message_name: str, message_type: str) -> Message: 11 | hl7_set_version("2.5.1") 12 | m = Message(message_name) 13 | msg_datetime = datetime.now().strftime("%Y%m%d%H%M%S") 14 | m.MSH.MSH_7 = msg_datetime 15 | m.MSH.MSH_9 = message_type 16 | m.MSH.MSH_10 = uuid4().hex 17 | m.MSH.MSH_11 = "T" 18 | m.MSH.MSH_16 = "AL" 19 | 20 | return m 21 | 22 | 23 | def create_adt_message(fhir_resource: dict) -> str: 24 | m = _create_hl7_message("ADT_A05", "ADT^A28^ADT_A05") 25 | m.PID = _create_pid_segment(fhir_resource) 26 | 27 | if contact_list := fhir_resource.get("contact"): 28 | for nk1_set_id, contact in enumerate(contact_list): 29 | nk1 = m.add_segment("NK1") 30 | _populate_nk1_segment(nk1, contact, nk1_set_id + 1) 31 | 32 | return m.to_er7() 33 | 34 | 35 | def _create_pid_segment(fhir_resource: dict) -> Segment: 36 | pid = Segment("PID") 37 | pid.PID_1 = str(1) 38 | 39 | for id_ in fhir_resource.get("identifier"): 40 | id_value = id_.get("value", "") 41 | id_type = id_.get("type", {}) 42 | 43 | if any([id_value, id_type]): 44 | pid_3 = pid.add_field("PID_3") 45 | pid_3.PID_3_1 = id_value 46 | pid_3.PID_3_4 = id_.get("system", "") 47 | if id_type_coding := id_type.get("coding"): 48 | pid_3.PID_3_5 = id_type_coding[0].get("code", "") 49 | 50 | if assigner := id_.get("assigner"): 51 | pid_3.PID_3_9 = assigner.get("display", "") 52 | 53 | if name_list := fhir_resource.get("name"): 54 | for name in name_list: 55 | pid_5 = pid.add_field("PID_5") 56 | _populate_name_field(pid_5, name) 57 | 58 | pid.PID_7 = fhir_resource.get("birthDate", "") 59 | pid.PID_8 = fhir_resource.get("gender", "") 60 | 61 | if address_list := fhir_resource.get("address"): 62 | for address in address_list: 63 | pid_11 = pid.add_field("PID_11") 64 | _populate_address_field(pid_11, address) 65 | if address_county := address.get("district"): 66 | pid_12 = pid.add_field("PID_12") 67 | pid_12.value = address_county 68 | 69 | if telecom_list := fhir_resource.get("telecom"): 70 | _populate_telecom_fields(pid, "PID_13", "PID_14", telecom_list) 71 | 72 | if patient_communication_list := fhir_resource.get("communication"): 73 | for patient_communication in patient_communication_list: 74 | pid_15 = pid.add_field("PID_15") 75 | pid_15.value = patient_communication.get("language", {}).get("text", "") 76 | 77 | pid.PID_16 = fhir_resource.get("maritalStatus", {}).get("text", "") 78 | 79 | return pid 80 | 81 | 82 | def _populate_nk1_segment(nk1: Segment, contact: dict, set_id: int) -> None: 83 | nk1.NK1_1 = str(set_id) 84 | if name := contact.get("name"): 85 | nk1_2 = Field("NK1_2") 86 | _populate_name_field(nk1_2, name) 87 | nk1.add(nk1_2) 88 | if address := contact.get("address"): 89 | nk1_4 = nk1.add_field("NK1_4") 90 | _populate_address_field(nk1_4, address) 91 | if telecom_list := contact.get("telecom"): 92 | _populate_telecom_fields(nk1, "NK1_5", "NK1_6", telecom_list) 93 | if relationship := contact.get("relationship"): 94 | nk1.NK1_7 = relationship[0].get("coding", [""])[0].get("code", "") 95 | 96 | 97 | def _populate_name_field(name_field: Field, name: dict) -> None: 98 | name_field.XPN_1 = name.get("family", "") 99 | if given_name := name.get("given"): 100 | name_field.XPN_2 = given_name[0] 101 | if len(given_name) > 1: 102 | name_field.XPN_3 = " ".join(given_name[1:]) 103 | name_field.XPN_5 = name.get("prefix", [""])[0] 104 | name_field.XPN_7 = _get_name_type_code(name.get("use")) 105 | 106 | 107 | def _get_name_type_code(value: str) -> str: 108 | mapping = dict( 109 | usual="U", 110 | official="L", 111 | temp="U", 112 | nickname="N", 113 | anonymous="S", 114 | old="U", 115 | maiden="M", 116 | ) 117 | return mapping.get(value, "") 118 | 119 | 120 | def _populate_address_field(address_field: Field, address: dict) -> None: 121 | if address_line_list := address.get("line"): 122 | address_field.XAD_1 = address_line_list[0] 123 | if len(address_line_list) > 1: 124 | address_field.XAD_2 = address_line_list[1] 125 | address_field.XAD_3 = address.get("city", "") 126 | address_field.XAD_4 = address.get("state", "") 127 | address_field.XAD_5 = address.get("postalCode", "") 128 | address_field.XAD_6 = address.get("country", "") 129 | address_field.XAD_7 = address.get("use", "") 130 | 131 | 132 | def _populate_telecom_fields( 133 | segment: Segment, personal_telecom: str, work_telecom: str, telecom_list 134 | ) -> None: 135 | for telecom in telecom_list: 136 | telecom_type = telecom.get("use", "") 137 | telecom_value = telecom.get("value", "") 138 | if telecom_type in ["home", "temp", "old", "mobile", ""]: 139 | telecom = personal_telecom 140 | else: 141 | telecom = work_telecom 142 | if telecom_value: 143 | telecom_field = Field(telecom) 144 | telecom_field.XTN_2 = _get_telecom_use_code(telecom_type) 145 | telecom_field.XTN_12 = telecom_value 146 | segment.add(telecom_field) 147 | 148 | 149 | def _get_telecom_use_code(telecom_type: str) -> str: 150 | mapping = dict( 151 | home="PRN", 152 | temp="TMP", 153 | old="OLD", 154 | mobile="MOB", 155 | work="WPN", 156 | ) 157 | return mapping.get(telecom_type, "") 158 | -------------------------------------------------------------------------------- /fhir-hl7-transform/lambda/lib/hl7_message_parser.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | from hl7apy.core import Field 5 | from hl7apy.parser import parse_message 6 | 7 | 8 | def parse_oru_message(hl7msg: str, r: dict) -> dict: 9 | # This function will be implemented in the next release 10 | return r 11 | 12 | 13 | def parse_adt_message(hl7msg: str, r: dict) -> dict: 14 | m = parse_message(hl7msg, find_groups=False) 15 | 16 | identifier_list = list() 17 | for pid_3 in m.PID.PID_3: 18 | identifier_element = {} 19 | if value := pid_3.PID_3_1.value: 20 | identifier_element["value"] = value 21 | if system := pid_3.PID_3_4.value: 22 | identifier_element["system"] = system 23 | if code := pid_3.PID_3_5.value: 24 | identifier_element["type"] = dict(coding=[dict(code=code)]) 25 | if assigner := pid_3.PID_3_9.value: 26 | identifier_element["assigner"] = dict(display=assigner) 27 | identifier_list.append(identifier_element) 28 | r["identifier"] = identifier_list 29 | 30 | if pid_5 := m.PID.PID_5: 31 | r["name"] = list() 32 | for pid_5_rep in pid_5: 33 | r["name"].append(_parse_name_field(pid_5_rep)) 34 | 35 | if birth_date := m.PID.PID_7.value: 36 | r["birthDate"] = birth_date 37 | 38 | if gender := m.PID.PID_8.value: 39 | r["gender"] = gender 40 | 41 | if pid_11 := m.PID.PID_11: 42 | r["address"] = list() 43 | for pid_11_rep in pid_11: 44 | r["address"].append(_parse_address_field(pid_11_rep)) 45 | 46 | if pid_12 := m.PID.PID_12: 47 | for rep, address_county in enumerate(pid_12): 48 | r["address"][rep]["district"] = address_county.value 49 | 50 | pid_13, pid_14 = m.PID.PID_13, m.PID.PID_14 51 | if pid_13 or pid_14: 52 | r["telecom"] = _parse_telecom_fields(pid_13, pid_14) 53 | 54 | if m.NK1: 55 | # parse NK1 segments 56 | contact_list = list() 57 | for nk1 in m.NK1: 58 | contact = dict() 59 | if nk1_2 := nk1.NK1_2: 60 | contact["name"] = _parse_name_field(nk1_2) 61 | if nk1_4 := nk1.NK1_4: 62 | contact["address"] = _parse_address_field(nk1_4) 63 | nk1_5, nk1_6 = nk1.NK1_5, nk1.NK1_6 64 | if nk1_5 or nk1_6: 65 | contact["telecom"] = _parse_telecom_fields(nk1_5, nk1_6) 66 | if nk1_7 := nk1.NK1_7.value: 67 | contact["relationship"] = [dict(coding=[dict(code=nk1_7)])] 68 | contact_list.append(contact) 69 | r["contact"] = contact_list 70 | 71 | return r 72 | 73 | 74 | def _parse_telecom_fields( 75 | personal_telecom_field: Field, work_telecom_field: Field 76 | ) -> list: 77 | telecom_list = list() 78 | for field in [personal_telecom_field, work_telecom_field]: 79 | if field: 80 | for rep in field: 81 | t = dict() 82 | if use_value := _get_telecom_use_value(rep.XTN_2.value): 83 | t["use"] = use_value 84 | if value := rep.XTN_12.value: 85 | t["value"] = value 86 | telecom_list.append(t) 87 | 88 | return telecom_list 89 | 90 | 91 | def _get_telecom_use_value(use_code: str) -> str: 92 | mapping = dict( 93 | PRN="home", 94 | TMP="temp", 95 | OLD="old", 96 | MOB="mobile", 97 | WPN="work", 98 | ) 99 | return mapping.get(use_code, "") 100 | 101 | 102 | def _parse_name_field(name_field: Field) -> list: 103 | name_entry = dict() 104 | if family_name := name_field.XPN_1.value: 105 | name_entry["family"] = family_name 106 | given_name = list() 107 | if first_given_name := name_field.XPN_2.value: 108 | given_name.append(first_given_name) 109 | if other_given_names := name_field.XPN_3.value: 110 | for given_name_component in other_given_names.split(" "): 111 | given_name.append(given_name_component) 112 | if given_name: 113 | name_entry["given"] = given_name 114 | if prefix := name_field.XPN_5.value: 115 | name_entry["prefix"] = [prefix] 116 | if use := _get_name_type_value(name_field.XPN_7.value): 117 | name_entry["use"] = use 118 | return name_entry 119 | 120 | 121 | def _parse_address_field(address_field: Field) -> list: 122 | address_list_entry = dict() 123 | address_line = list() 124 | if first_address_line := address_field.XAD_1.value: 125 | address_line.append(first_address_line) 126 | if second_address_line := address_field.XAD_2.value: 127 | address_line.append(second_address_line) 128 | if address_line: 129 | address_list_entry["line"] = address_line 130 | if city := address_field.XAD_3.value: 131 | address_list_entry["city"] = city 132 | if state := address_field.XAD_4.value: 133 | address_list_entry["state"] = state 134 | if postal_code := address_field.XAD_5.value: 135 | address_list_entry["postalCode"] = postal_code 136 | if country := address_field.XAD_6.value: 137 | address_list_entry["country"] = country 138 | if use := address_field.XAD_7.value: 139 | address_list_entry["use"] = use 140 | 141 | return address_list_entry 142 | 143 | 144 | def _get_name_type_value(value: str) -> str: 145 | mapping = dict( 146 | U="usual", 147 | L="official", 148 | N="nickname", 149 | S="anonymous", 150 | M="maiden", 151 | ) 152 | return mapping.get(value, "") 153 | -------------------------------------------------------------------------------- /fhir-hl7-transform/lambda/lib/hl7_to_fhir.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | from lib.hl7_message_parser import parse_adt_message, parse_oru_message 5 | 6 | 7 | class Hl7v2ToFhirConverter(object): 8 | """ 9 | This class converts HL7v2 messages to FHIR resource format using JSON 10 | """ 11 | 12 | def __init__(self, hl7msg: str, resource_type: str, resource_id: str) -> None: 13 | self._hl7msg = hl7msg 14 | self._resource_type = resource_type 15 | self._resource_id = resource_id 16 | 17 | def transform(self) -> dict: 18 | r = dict() 19 | r["resourceType"] = self._resource_type 20 | r["id"] = self._resource_id 21 | if self._resource_type == "Patient": 22 | resource = parse_adt_message(self._hl7msg, r) 23 | elif self._resource_type == "Observation": 24 | resource = parse_oru_message(self._hl7msg, r) 25 | else: 26 | return {} 27 | 28 | return resource 29 | -------------------------------------------------------------------------------- /fhir-hl7-transform/lambda/requirements.txt: -------------------------------------------------------------------------------- 1 | hl7apy 2 | -------------------------------------------------------------------------------- /fhir-hl7-transform/lambda/transform.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import json 5 | import logging 6 | import os 7 | from base64 import b64decode 8 | from typing import Any 9 | 10 | import boto3 11 | 12 | from lib.fhir_resource_reader import FhirResourceReader 13 | from lib.fhir_resource_writer import FhirResourceWriter 14 | 15 | logger = logging.getLogger(__name__) 16 | logger.setLevel(logging.INFO) 17 | 18 | 19 | def handler(event, context): 20 | sqs_queue = os.environ.get("SQS_QUEUE") 21 | s3_bucket_name = os.environ.get("S3_BUCKET_NAME") 22 | 23 | if (sqs_queue is None) or (s3_bucket_name is None): 24 | logger.error("Check SQS_QUEUE or S3_BUCKET_NAME environment variables") 25 | return prepare_response(500, {}, "Configuration Error") 26 | 27 | http_method = event.get("httpMethod") 28 | 29 | # Write request 30 | if http_method in ["POST", "PUT"]: 31 | fhir_resource_content = parse_event(event) 32 | try: 33 | hl7v2_message, resource = FhirResourceWriter( 34 | fhir_resource_content, 35 | event.get("pathParameters") if http_method == "PUT" else None, 36 | ).write() 37 | except Exception as exc: 38 | path_parameters = event.get("pathParameters", {}) 39 | resource_type = path_parameters.get("resource_type", "") 40 | status_code = 400 41 | resource = {} 42 | message = f"Unable to parse resource {resource_type}" 43 | logger.exception(message, exc_info=exc) 44 | else: 45 | try: 46 | send_hl7_to_transporter(sqs_queue, hl7v2_message) 47 | status_code = 201 48 | message = "" 49 | except Exception as exc: 50 | status_code = 500 51 | resource = {} 52 | message = "Unable to pass request to back end system" 53 | logger.exception(message, exc_info=exc) 54 | 55 | # Read request implemented in this proof of concept relies 56 | # on mock HL7 server implementation which stores HL7 messages 57 | # as S3 objects 58 | elif http_method == "GET": 59 | try: 60 | resource = FhirResourceReader( 61 | s3_bucket_name, event.get("pathParameters") 62 | ).read() 63 | status_code = 200 64 | message = "" 65 | except Exception as exc: 66 | path_parameters = event.get("pathParameters", {}) 67 | resource_type = path_parameters.get("resource_type", "") 68 | id = path_parameters.get("id", "") 69 | status_code = 404 70 | resource = {} 71 | message = f"Unable to find resource {resource_type} with {id}" 72 | logger.error(f"Resource {resource_type}/{id} not found", exc_info=exc) 73 | 74 | # Delete 75 | elif http_method == "DELETE": 76 | status_code = 400 77 | resource = {} 78 | message = "We currently do not support DELETE requests" 79 | 80 | # Unknown method 81 | else: 82 | status_code = 501 83 | resource = {} 84 | message = f"Unknown method: {http_method}" 85 | logger.error(message) 86 | 87 | return prepare_response(status_code, resource, message) 88 | 89 | 90 | def send_hl7_to_transporter(sqs_queue: str, message: Any) -> None: 91 | sqs = boto3.client("sqs") 92 | sqs.send_message(QueueUrl=sqs_queue, MessageBody=message) 93 | 94 | 95 | def parse_event(event: Any) -> Any: 96 | if event.get("isBase64Encoded"): 97 | return json.loads(b64decode(event["body"])) 98 | else: 99 | return json.loads(event["body"]) 100 | 101 | 102 | def prepare_response(status_code: int, resource: Any, message: str) -> dict: 103 | body = {"resource": resource, "message": message} 104 | return { 105 | "statusCode": status_code, 106 | "body": json.dumps(body), 107 | } 108 | -------------------------------------------------------------------------------- /resources/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/fhir-hl7v2-integration-transform/a34d9c406b031aff362ee18bea36aacddbb74e32/resources/architecture.png -------------------------------------------------------------------------------- /resources/patient.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Patient", 3 | "identifier": [{ 4 | "value": "12345", 5 | "system": "urn:oid:1.2.36.146.595.217.0.1", 6 | "type": { 7 | "coding": [{ 8 | "code": "MR" 9 | }] 10 | }, 11 | "assigner": { 12 | "display": "Acme Healthcare" 13 | } 14 | }, 15 | { 16 | "value": "789045", 17 | "system": "urn:oid:1.2.36.146.595.217.0.1", 18 | "type": { 19 | "coding": [{ 20 | "code": "MR" 21 | }] 22 | } 23 | } 24 | ], 25 | "name": [{ 26 | "family": "Chalmers", 27 | "given": [ 28 | "Peter", 29 | "James" 30 | ], 31 | "use": "official" 32 | }, 33 | { 34 | "given": [ 35 | "Jim" 36 | ], 37 | "use": "usual" 38 | }, 39 | { 40 | "family": "Simpson", 41 | "given": [ 42 | "Peter", 43 | "James", 44 | "Homer" 45 | ], 46 | "use": "maiden" 47 | } 48 | ], 49 | "birthDate": "1974-12-25", 50 | "gender": "male", 51 | "address": [{ 52 | "line": [ 53 | "534 Erewhon St", 54 | "Apt 3-A" 55 | ], 56 | "city": "PleasantVille", 57 | "state": "Vic", 58 | "postalCode": "3999", 59 | "use": "home", 60 | "district": "Rainbow" 61 | }, 62 | { 63 | "line": [ 64 | "534 Erewhon St" 65 | ], 66 | "city": "PleasantVille", 67 | "state": "Vic", 68 | "postalCode": "3999", 69 | "use": "work", 70 | "district": "Rainbow" 71 | } 72 | ], 73 | "telecom": [{ 74 | "use": "mobile", 75 | "value": "(03) 3410 5613" 76 | }, 77 | { 78 | "use": "old", 79 | "value": "(03) 5555 8834" 80 | }, 81 | { 82 | "use": "work", 83 | "value": "(03) 5555 6473" 84 | } 85 | ], 86 | "contact": [{ 87 | "name": [{ 88 | "family": "du Marché", 89 | "given": [ 90 | "Bénédicte" 91 | ] 92 | }], 93 | "address": [{ 94 | "line": [ 95 | "534 Erewhon St" 96 | ], 97 | "city": "PleasantVille", 98 | "state": "Vic", 99 | "postalCode": "3999", 100 | "use": "home" 101 | }], 102 | "telecom": [{ 103 | "value": "+33 (237) 998327" 104 | }], 105 | "relationship": [{ 106 | "coding": [{ 107 | "code": "N" 108 | }] 109 | }] 110 | }, 111 | { 112 | "name": [{ 113 | "family": "Smith", 114 | "given": [ 115 | "Jane", 116 | "Marie" 117 | ] 118 | }] 119 | } 120 | ] 121 | } 122 | -------------------------------------------------------------------------------- /test-hl7-server/cdk-infra/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | package-lock.json 3 | __pycache__ 4 | .pytest_cache 5 | .env 6 | *.egg-info 7 | 8 | # CDK asset staging directory 9 | .cdk.staging 10 | cdk.out 11 | cdk.context.json 12 | -------------------------------------------------------------------------------- /test-hl7-server/cdk-infra/README.md: -------------------------------------------------------------------------------- 1 | ### HL7 Server 2 | The purpose of this optional CDK app is to deploy stack implementing HL7 server used to demonstrate ability of exemplar Integration Transform communicate with third-party systems. -------------------------------------------------------------------------------- /test-hl7-server/cdk-infra/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | import os 5 | from aws_cdk import core 6 | 7 | from cdk_infra.test_hl7_server_stack import TestHl7Stack 8 | 9 | 10 | app = core.App() 11 | TestHl7Stack( 12 | app, 13 | "test-hl7-server-stack", 14 | stack_name="test-hl7-server-stack", 15 | env=core.Environment( 16 | account=os.environ["CDK_DEFAULT_ACCOUNT"], 17 | region=os.environ["CDK_DEFAULT_REGION"], 18 | ), 19 | ) 20 | 21 | app.synth() 22 | -------------------------------------------------------------------------------- /test-hl7-server/cdk-infra/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 app.py", 3 | "context": { 4 | "@aws-cdk/core:enableStackNameDuplicates": "true", 5 | "aws-cdk:enableDiffNoFail": "true", 6 | "@aws-cdk/core:stackRelativeExports": "true" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test-hl7-server/cdk-infra/cdk_infra/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/fhir-hl7v2-integration-transform/a34d9c406b031aff362ee18bea36aacddbb74e32/test-hl7-server/cdk-infra/cdk_infra/__init__.py -------------------------------------------------------------------------------- /test-hl7-server/cdk-infra/cdk_infra/test_hl7_server_stack.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | from os import path 5 | 6 | from aws_cdk import aws_ecs as ecs 7 | from aws_cdk import aws_ec2 as ec2 8 | from aws_cdk import aws_ecs_patterns as ecs_patterns 9 | from aws_cdk import aws_iam as iam 10 | from aws_cdk import aws_s3 as s3 11 | from aws_cdk import core 12 | 13 | COMPONENT_PREFIX = "Hl7TestServer" 14 | 15 | dirname = path.dirname(__file__) 16 | 17 | 18 | class TestHl7Stack(core.Stack): 19 | def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: 20 | super().__init__(scope, id, **kwargs) 21 | 22 | vpc_id = self.node.try_get_context("vpc-id") 23 | if _server_port := (self.node.try_get_context("server-port")): 24 | server_port = int(_server_port) 25 | else: 26 | server_port = 2575 27 | 28 | # S3 Bucket to store and retrieve HL7v2 messages 29 | test_server_output_bucket = s3.Bucket( 30 | self, 31 | f"{COMPONENT_PREFIX}OutputBucket", 32 | encryption=s3.BucketEncryption.S3_MANAGED, 33 | block_public_access=s3.BlockPublicAccess.BLOCK_ALL, 34 | ) 35 | 36 | vpc = ec2.Vpc.from_lookup(self, "EcsVpc", vpc_id=vpc_id) 37 | 38 | # cluster = ecs.Cluster(self, f"{COMPONENT_PREFIX}", vpc=vpc) 39 | 40 | # Test receiver (server) 41 | nlb_service = ecs_patterns.NetworkLoadBalancedFargateService( 42 | self, 43 | f"{COMPONENT_PREFIX}Service", 44 | # cluster=cluster, 45 | task_image_options={ 46 | "image": ecs.ContainerImage.from_asset( 47 | path.join(dirname, "../../container") 48 | ), 49 | "container_port": server_port, 50 | "enable_logging": True, 51 | "environment": { 52 | "S3_BUCKET_NAME": test_server_output_bucket.bucket_name, 53 | "PORT_NUMBER": str(server_port), 54 | }, 55 | "container_name": "hl7server", 56 | }, 57 | desired_count=1, 58 | listener_port=server_port, 59 | public_load_balancer=False, 60 | vpc=vpc, 61 | ) 62 | service = nlb_service.service 63 | connections = service.connections 64 | connections.allow_from( 65 | ec2.Peer.ipv4(vpc.vpc_cidr_block), 66 | ec2.Port.tcp(server_port), 67 | "Allow inbound HL7 connections", 68 | ) 69 | task_definition = service.task_definition 70 | test_server_output_bucket.add_to_resource_policy( 71 | permission=iam.PolicyStatement( 72 | actions=["s3:ListBucket", "s3:PutObject"], 73 | effect=iam.Effect.ALLOW, 74 | principals=[task_definition.task_role], 75 | resources=[ 76 | test_server_output_bucket.bucket_arn, 77 | test_server_output_bucket.arn_for_objects("*"), 78 | ], 79 | ) 80 | ) 81 | task_definition.add_to_task_role_policy( 82 | iam.PolicyStatement( 83 | actions=["s3:ListBucket"], 84 | effect=iam.Effect.ALLOW, 85 | resources=[test_server_output_bucket.bucket_arn], 86 | ) 87 | ) 88 | task_definition.add_to_task_role_policy( 89 | iam.PolicyStatement( 90 | actions=["s3:PutObject"], 91 | effect=iam.Effect.ALLOW, 92 | resources=[test_server_output_bucket.arn_for_objects("*")], 93 | ) 94 | ) 95 | 96 | core.CfnOutput( 97 | self, 98 | "TestHl7ServerFQDN", 99 | value=nlb_service.load_balancer.load_balancer_dns_name, 100 | export_name="TestHl7ServerFQDN", 101 | ) 102 | 103 | core.CfnOutput( 104 | self, 105 | "TestHl7ServerS3", 106 | value=test_server_output_bucket.bucket_name, 107 | export_name="TestHl7ServerBucket", 108 | ) 109 | -------------------------------------------------------------------------------- /test-hl7-server/cdk-infra/requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | -------------------------------------------------------------------------------- /test-hl7-server/cdk-infra/setup.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import setuptools 5 | 6 | 7 | with open("README.md") as fp: 8 | long_description = fp.read() 9 | 10 | 11 | setuptools.setup( 12 | name="cdk_infra", 13 | version="0.0.1", 14 | description="CDK Python app implementing HL7 server", 15 | long_description=long_description, 16 | long_description_content_type="text/markdown", 17 | author="author", 18 | package_dir={"": "cdk_infra"}, 19 | packages=setuptools.find_packages(where="cdk_infra"), 20 | install_requires=[ 21 | "aws-cdk.core==1.68.0", 22 | "aws-cdk.aws_s3==1.68.0", 23 | "aws-cdk.aws_ecs==1.68.0", 24 | "aws-cdk.aws_ecs_patterns==1.68.0", 25 | ], 26 | python_requires=">=3.6", 27 | classifiers=[ 28 | "Development Status :: 4 - Beta", 29 | "Intended Audience :: Developers", 30 | "License :: OSI Approved :: Apache Software License", 31 | "Programming Language :: JavaScript", 32 | "Programming Language :: Python :: 3 :: Only", 33 | "Programming Language :: Python :: 3.6", 34 | "Programming Language :: Python :: 3.7", 35 | "Programming Language :: Python :: 3.8", 36 | "Topic :: Software Development :: Code Generators", 37 | "Topic :: Utilities", 38 | "Typing :: Typed", 39 | ], 40 | ) 41 | -------------------------------------------------------------------------------- /test-hl7-server/cdk-infra/source.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem The sole purpose of this script is to make the command 4 | rem 5 | rem source .env/bin/activate 6 | rem 7 | rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. 8 | rem On Windows, this command just runs this batch file (the argument is ignored). 9 | rem 10 | rem Now we don't need to document a Windows command for activating a virtualenv. 11 | 12 | echo Executing .env\Scripts\activate.bat for you 13 | .env\Scripts\activate.bat 14 | -------------------------------------------------------------------------------- /test-hl7-server/container/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # Container image to run test HL7v2 server (receiver) 4 | FROM python:3.8 5 | 6 | WORKDIR /application 7 | 8 | COPY requirements.txt . 9 | 10 | RUN pip install --no-cache-dir -r requirements.txt 11 | 12 | COPY application/ . 13 | 14 | EXPOSE 2575 15 | 16 | CMD [ "python", "hl7_listener.py" ] 17 | -------------------------------------------------------------------------------- /test-hl7-server/container/application/hl7_listener.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | import logging 4 | import os 5 | import signal 6 | 7 | import boto3 8 | from twisted.internet import defer, reactor 9 | from txHL7.mllp import MLLPFactory 10 | from txHL7.receiver import AbstractHL7Receiver 11 | 12 | logger = logging.getLogger() 13 | logger.setLevel(logging.INFO) 14 | 15 | s3_bucket_name = os.environ["S3_BUCKET_NAME"] 16 | port = int(os.environ.get("PORT_NUMBER", "2575")) 17 | 18 | resource_type = { 19 | "ADT": "Patient", 20 | "ORU": "Observation", 21 | } 22 | 23 | 24 | class HL7Receiver(AbstractHL7Receiver): 25 | def handleMessage(self, container): 26 | message = container.message 27 | 28 | message_type = str(message["MSH.F9"]) 29 | resource_id = None 30 | for pid_3 in message.segment("PID")[3]: 31 | if len(pid_3) >= 5 and str(pid_3[4]) == "FW": 32 | resource_id = str(pid_3[0]) 33 | break 34 | 35 | if resource_id is None: 36 | raise Exception("Unable to get resource ID from the message") 37 | 38 | print(f"Received message {message_type} id: [{resource_id}]", flush=True) 39 | 40 | try: 41 | s3 = boto3.resource("s3") 42 | s3_object = s3.Object( 43 | s3_bucket_name, 44 | f"{resource_type.get(str(message_type), 'Other')}/{resource_id}", 45 | ) 46 | s3_object.put(Body=str.encode(str(message))) 47 | except Exception as e: 48 | logger.exception(f"Exception: {repr(e)}", exc_info=e) 49 | raise (e) 50 | else: 51 | # We succeeded, so ACK back (default is AA) 52 | return defer.succeed(container.ack()) 53 | 54 | 55 | def handler(signum, frame): 56 | logger.info(f"Signal {signum} caught.") 57 | 58 | 59 | def run(port): 60 | signal.signal(signal.SIGINT, handler) 61 | signal.signal(signal.SIGTERM, handler) 62 | 63 | receiver = HL7Receiver() 64 | factory = MLLPFactory(receiver) 65 | 66 | reactor.listenTCP(port, factory) 67 | print(f"Listening on port {port}", flush=True) 68 | reactor.run() 69 | 70 | 71 | if __name__ == "__main__": 72 | run(port) 73 | -------------------------------------------------------------------------------- /test-hl7-server/container/requirements.txt: -------------------------------------------------------------------------------- 1 | twisted 2 | txHL7 3 | hl7 4 | boto3 5 | --------------------------------------------------------------------------------