epoc2etsi 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- epoc2etsi-0.1.0/.gitignore +10 -0
- epoc2etsi-0.1.0/.python-version +1 -0
- epoc2etsi-0.1.0/PKG-INFO +102 -0
- epoc2etsi-0.1.0/README.md +93 -0
- epoc2etsi-0.1.0/pyproject.toml +19 -0
- epoc2etsi-0.1.0/src/epoc2etsi/__init__.py +57 -0
- epoc2etsi-0.1.0/src/epoc2etsi/forms/form1.py +241 -0
- epoc2etsi-0.1.0/src/epoc2etsi/forms/form2.py +195 -0
- epoc2etsi-0.1.0/src/epoc2etsi/forms/form3.py +78 -0
- epoc2etsi-0.1.0/src/epoc2etsi/forms/form5.py +41 -0
- epoc2etsi-0.1.0/src/epoc2etsi/forms/form6.py +44 -0
- epoc2etsi-0.1.0/src/epoc2etsi/forms/mappings.py +34 -0
- epoc2etsi-0.1.0/src/epoc2etsi/hi1/__init__.py +0 -0
- epoc2etsi-0.1.0/src/epoc2etsi/hi1/generators.py +149 -0
- epoc2etsi-0.1.0/src/epoc2etsi/hi1/helpers.py +28 -0
- epoc2etsi-0.1.0/src/epoc2etsi/namespaces.py +41 -0
- epoc2etsi-0.1.0/src/epoc2etsi/xml.py +18 -0
- epoc2etsi-0.1.0/uv.lock +56 -0
- epoc2etsi-0.1.0/validate.bat +5 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
epoc2etsi-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: epoc2etsi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author-email: mark <markc@tencastle.com>
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: lxml
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# Epoc2etsi
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## Getting started
|
|
15
|
+
|
|
16
|
+
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
|
17
|
+
|
|
18
|
+
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
|
|
19
|
+
|
|
20
|
+
## Add your files
|
|
21
|
+
|
|
22
|
+
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
|
|
23
|
+
- [ ] [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
cd existing_repo
|
|
27
|
+
git remote add origin https://forge.etsi.org/rep/canterburym/epoc2etsi.git
|
|
28
|
+
git branch -M main
|
|
29
|
+
git push -uf origin main
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Integrate with your tools
|
|
33
|
+
|
|
34
|
+
- [ ] [Set up project integrations](https://forge.etsi.org/rep/canterburym/epoc2etsi/-/settings/integrations)
|
|
35
|
+
|
|
36
|
+
## Collaborate with your team
|
|
37
|
+
|
|
38
|
+
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
|
|
39
|
+
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
|
|
40
|
+
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
|
|
41
|
+
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
|
|
42
|
+
- [ ] [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
|
|
43
|
+
|
|
44
|
+
## Test and Deploy
|
|
45
|
+
|
|
46
|
+
Use the built-in continuous integration in GitLab.
|
|
47
|
+
|
|
48
|
+
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/)
|
|
49
|
+
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
|
|
50
|
+
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
|
|
51
|
+
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
|
|
52
|
+
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
|
|
53
|
+
|
|
54
|
+
***
|
|
55
|
+
|
|
56
|
+
# Editing this README
|
|
57
|
+
|
|
58
|
+
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
|
|
59
|
+
|
|
60
|
+
## Suggestions for a good README
|
|
61
|
+
|
|
62
|
+
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
|
|
63
|
+
|
|
64
|
+
## Name
|
|
65
|
+
Choose a self-explaining name for your project.
|
|
66
|
+
|
|
67
|
+
## Description
|
|
68
|
+
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
|
|
69
|
+
|
|
70
|
+
## Badges
|
|
71
|
+
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
|
|
72
|
+
|
|
73
|
+
## Visuals
|
|
74
|
+
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
|
|
81
|
+
|
|
82
|
+
## Support
|
|
83
|
+
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
|
84
|
+
|
|
85
|
+
## Roadmap
|
|
86
|
+
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
|
87
|
+
|
|
88
|
+
## Contributing
|
|
89
|
+
State if you are open to contributions and what your requirements are for accepting them.
|
|
90
|
+
|
|
91
|
+
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
|
|
92
|
+
|
|
93
|
+
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
|
|
94
|
+
|
|
95
|
+
## Authors and acknowledgment
|
|
96
|
+
Show your appreciation to those who have contributed to the project.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
For open source projects, say how it is licensed.
|
|
100
|
+
|
|
101
|
+
## Project status
|
|
102
|
+
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Epoc2etsi
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
## Getting started
|
|
6
|
+
|
|
7
|
+
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
|
8
|
+
|
|
9
|
+
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
|
|
10
|
+
|
|
11
|
+
## Add your files
|
|
12
|
+
|
|
13
|
+
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
|
|
14
|
+
- [ ] [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
cd existing_repo
|
|
18
|
+
git remote add origin https://forge.etsi.org/rep/canterburym/epoc2etsi.git
|
|
19
|
+
git branch -M main
|
|
20
|
+
git push -uf origin main
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Integrate with your tools
|
|
24
|
+
|
|
25
|
+
- [ ] [Set up project integrations](https://forge.etsi.org/rep/canterburym/epoc2etsi/-/settings/integrations)
|
|
26
|
+
|
|
27
|
+
## Collaborate with your team
|
|
28
|
+
|
|
29
|
+
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
|
|
30
|
+
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
|
|
31
|
+
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
|
|
32
|
+
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
|
|
33
|
+
- [ ] [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
|
|
34
|
+
|
|
35
|
+
## Test and Deploy
|
|
36
|
+
|
|
37
|
+
Use the built-in continuous integration in GitLab.
|
|
38
|
+
|
|
39
|
+
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/)
|
|
40
|
+
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
|
|
41
|
+
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
|
|
42
|
+
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
|
|
43
|
+
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
|
|
44
|
+
|
|
45
|
+
***
|
|
46
|
+
|
|
47
|
+
# Editing this README
|
|
48
|
+
|
|
49
|
+
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
|
|
50
|
+
|
|
51
|
+
## Suggestions for a good README
|
|
52
|
+
|
|
53
|
+
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
|
|
54
|
+
|
|
55
|
+
## Name
|
|
56
|
+
Choose a self-explaining name for your project.
|
|
57
|
+
|
|
58
|
+
## Description
|
|
59
|
+
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
|
|
60
|
+
|
|
61
|
+
## Badges
|
|
62
|
+
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
|
|
63
|
+
|
|
64
|
+
## Visuals
|
|
65
|
+
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
|
|
72
|
+
|
|
73
|
+
## Support
|
|
74
|
+
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
|
75
|
+
|
|
76
|
+
## Roadmap
|
|
77
|
+
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
|
78
|
+
|
|
79
|
+
## Contributing
|
|
80
|
+
State if you are open to contributions and what your requirements are for accepting them.
|
|
81
|
+
|
|
82
|
+
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
|
|
83
|
+
|
|
84
|
+
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
|
|
85
|
+
|
|
86
|
+
## Authors and acknowledgment
|
|
87
|
+
Show your appreciation to those who have contributed to the project.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
For open source projects, say how it is licensed.
|
|
91
|
+
|
|
92
|
+
## Project status
|
|
93
|
+
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "epoc2etsi"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "mark", email = "markc@tencastle.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"lxml"
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
epoc2etsi = "epoc2etsi:main"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["hatchling"]
|
|
19
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from sys import argv
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from lxml import objectify
|
|
8
|
+
|
|
9
|
+
from .hi1.generators import *
|
|
10
|
+
from .hi1.helpers import etsify_datetime
|
|
11
|
+
from .xml import xml
|
|
12
|
+
|
|
13
|
+
from .forms import form1, form2, form3, form5, form6
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
sender_cc = "XX"
|
|
17
|
+
sender_id = "RI_API"
|
|
18
|
+
receiver_cc = "XX"
|
|
19
|
+
receiver_id = "ServiceProviderID"
|
|
20
|
+
|
|
21
|
+
def main() -> None:
|
|
22
|
+
if len(argv) < 2:
|
|
23
|
+
logging.error("Please specify an input file")
|
|
24
|
+
exit(-1)
|
|
25
|
+
|
|
26
|
+
input_path = Path(argv[1])
|
|
27
|
+
input_obj = objectify.parse(input_path, parser=None)
|
|
28
|
+
|
|
29
|
+
input_root = input_obj.getroot()
|
|
30
|
+
global_case_id = input_root.globalCaseId.text
|
|
31
|
+
form_id = input_root.formId
|
|
32
|
+
parent_form_id = input_root.parentFormId if hasattr(input_root, "parentFormId") else None
|
|
33
|
+
form_obj = input_root.form.getchildren()[0]
|
|
34
|
+
ri_to_sp = True
|
|
35
|
+
|
|
36
|
+
hi1 = None
|
|
37
|
+
|
|
38
|
+
match form_obj.tag.split("}")[1]:
|
|
39
|
+
case "epocForm1" : hi1 = form1.processForm1(form_obj, global_case_id, form_id)
|
|
40
|
+
case "epocPrForm2" : hi1 = form2.processForm2(form_obj, global_case_id, form_id)
|
|
41
|
+
case "epocForm3" :
|
|
42
|
+
hi1 = form3.processForm3(form_obj, global_case_id, form_id, parent_form_id)
|
|
43
|
+
ri_to_sp = False
|
|
44
|
+
case "epocPrForm5" : hi1 = form5.processForm5(form_obj, global_case_id, form_id, parent_form_id)
|
|
45
|
+
case "epocPrForm6" : hi1 = form6.processForm6(form_obj, global_case_id, form_id, parent_form_id)
|
|
46
|
+
case _ :
|
|
47
|
+
print (f"No matching form found for {form_obj.tag}")
|
|
48
|
+
exit(-1)
|
|
49
|
+
|
|
50
|
+
hi1.Header.SenderIdentifier.CountryCode = sender_cc if ri_to_sp else receiver_cc
|
|
51
|
+
hi1.Header.SenderIdentifier.UniqueIdentifier = sender_id if ri_to_sp else receiver_id
|
|
52
|
+
hi1.Header.ReceiverIdentifier.CountryCode = receiver_cc if ri_to_sp else sender_cc
|
|
53
|
+
hi1.Header.ReceiverIdentifier.UniqueIdentifier = receiver_id if ri_to_sp else sender_id
|
|
54
|
+
hi1.Header.Timestamp = etsify_datetime(datetime.now())
|
|
55
|
+
hi1.Header.TransactionIdentifier = str(uuid4())
|
|
56
|
+
|
|
57
|
+
print(xml(hi1))
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
from sys import argv
|
|
4
|
+
from datetime import datetime, date
|
|
5
|
+
|
|
6
|
+
from .mappings import *
|
|
7
|
+
|
|
8
|
+
from ..hi1.generators import *
|
|
9
|
+
from ..hi1.helpers import *
|
|
10
|
+
|
|
11
|
+
# ----------------------------------------------------------
|
|
12
|
+
# Authorisation object - clause 5.3.3
|
|
13
|
+
# ----------------------------------------------------------
|
|
14
|
+
def add_auth_object(hi1, input_xml, global_case_id, form_id):
|
|
15
|
+
ia_technical_id = input_xml.findall(f".//{{{EPOC_NS}}}Addressee//{{{EIO_NS}}}AuthorityTechnicalIdentifier")[0]
|
|
16
|
+
authObj = generate_AuthorisationObject(form_id, ia_technical_id.Country, ia_technical_id.NationalId)
|
|
17
|
+
generate_CREATE_Action(authObj, hi1)
|
|
18
|
+
|
|
19
|
+
# TODO - set authObject associated objects from Section D
|
|
20
|
+
|
|
21
|
+
add_child(authObj, "AuthorisationReference", global_case_id, ns = AUTH_NS)
|
|
22
|
+
add_dict_child(authObj, "AuthorisationLegalType", "ETSI", "EPOCLegalType", "EPOC", ns=AUTH_NS)
|
|
23
|
+
|
|
24
|
+
deadline_tag = input_xml.findall(f".//{{{FORM1_NS}}}DataProductionInRelationToDeadline")[0]
|
|
25
|
+
deadline_texts = [e.text for e in deadline_tag.getchildren()]
|
|
26
|
+
# TODO - The EC XSD allows both 10 day deadlines to be specified together. What does this mean?
|
|
27
|
+
# TODO - decide how to handle multiple deadlines
|
|
28
|
+
deadline = PRIORITY_MAPPING[deadline_texts[0]]
|
|
29
|
+
add_dict_child(authObj, "AuthorisationPriority", "ETSI", "EPOCPriority", deadline, ns=AUTH_NS)
|
|
30
|
+
|
|
31
|
+
add_dict_child(authObj, "AuthorisationDesiredStatus", "ETSI", "AuthorisationDesiredSatus", "SubmittedToCSP", ns=AUTH_NS)
|
|
32
|
+
|
|
33
|
+
sp_concerned = input_xml.findall(f".//{{{EPOC_NS}}}Addressee//{{{EPOC_NS}}}ServiceProviderConcerned/{{{EPOC_NS}}}TechnicalIdentifier")
|
|
34
|
+
if len(sp_concerned) > 0:
|
|
35
|
+
sp_concerned = sp_concerned[0]
|
|
36
|
+
auth_cspid = add_child(authObj, "AuthorisationCSPID", ns=AUTH_NS)
|
|
37
|
+
auth_cspid = add_child(auth_cspid, "CSPID", ns=AUTH_NS)
|
|
38
|
+
add_child(auth_cspid, "CountryCode", sp_concerned[f"{{{EIO_NS}}}Country"].text, ns=CORE_NS)
|
|
39
|
+
add_child(auth_cspid, "UniqueIdentifier", sp_concerned[f"{{{EIO_NS}}}NationalId"].text, ns=CORE_NS)
|
|
40
|
+
|
|
41
|
+
fill_in_approval_details(authObj, input_xml, is_validating_auth=False)
|
|
42
|
+
if input_xml.find(f".//{{{FORM1_NS}}}SectionJ") is not None:
|
|
43
|
+
fill_in_approval_details(authObj, input_xml, is_validating_auth=True)
|
|
44
|
+
|
|
45
|
+
flags = add_child(authObj, "AuthorisationFlags", ns=AUTH_NS)
|
|
46
|
+
emergency_flag = input_xml.find(f".//{{{FORM1_NS}}}SectionB/{{{EPOC_NS}}}EmergencyCase")
|
|
47
|
+
if emergency_flag is not None and emergency_flag.text.lower() == "true":
|
|
48
|
+
add_dict_child(flags, "AuthorisationFlag", "ETSI", "AuthorisationFlag", "IsEmergency", ns=AUTH_NS)
|
|
49
|
+
delay_flag = input_xml.find(f".//{{{FORM1_NS}}}SectionH/{{{FORM1_NS}}}DelayConditions")
|
|
50
|
+
if delay_flag is not None and len(delay_flag.getchildren()) > 0:
|
|
51
|
+
add_dict_child(flags, "AuthorisationFlag", "ETSI", "EPOCAuthorisationFlag", "DelayInformingUser", ns=AUTH_NS)
|
|
52
|
+
ea_notified = input_xml.find(f".//{{{FORM1_NS}}}SectionK/{{{FORM1_NS}}}EnforcingAuthority")
|
|
53
|
+
if ea_notified is not None:
|
|
54
|
+
add_dict_child(flags, "AuthorisationFlag", "ETSI", "EPOCAuthorisationFlag", "EnforcingAuthorityNotified", ns=AUTH_NS)
|
|
55
|
+
# TODO - there is a technical ID for the enforcing authority buried in <ns9:SectionK> - need to work out how to map it
|
|
56
|
+
|
|
57
|
+
map_child(authObj, "AuthorisationLegalEntity", input_xml, f".//{{{FORM1_NS}}}SectionB/{{{EPOC_NS}}}Addressee/{{{EPOC_NS}}}Authority/{{{EIO_NS}}}NameOfAuthority", ns=AUTH_NS)
|
|
58
|
+
return authObj
|
|
59
|
+
|
|
60
|
+
# Mapping as described in table 5.3.3.5 (issuing) table 5.3.3-8 (validating)
|
|
61
|
+
def fill_in_approval_details(parent, tree, is_validating_auth):
|
|
62
|
+
approval_details = add_child(parent, "AuthorisationApprovalDetails", ns=AUTH_NS)
|
|
63
|
+
|
|
64
|
+
if is_validating_auth:
|
|
65
|
+
input_section = tree.find(f".//{{{FORM1_NS}}}SectionJ/{{{EPOC_NS}}}Details")
|
|
66
|
+
else:
|
|
67
|
+
input_section = tree.find(f".//{{{EPOC_NS}}}IssuingAndContactAuthority/{{{EPOC_NS}}}IssuingAuthority")
|
|
68
|
+
|
|
69
|
+
add_child(approval_details, "ApprovalType", "ValidatingAuthority" if is_validating_auth else "IssuingAuthority", ns=COMMON_NS)
|
|
70
|
+
map_child(approval_details, "ApprovalReference", input_section, f"{{{EIO_NS}}}FileReference", ns=COMMON_NS)
|
|
71
|
+
fill_in_approver_details(approval_details, tree, input_section, is_validating_auth)
|
|
72
|
+
|
|
73
|
+
# TODO - there are some nasty edge-cases here to deal with re timezone offsets
|
|
74
|
+
# since the XML appears to have a timezone offset but no time, and ETSI-fying the dates
|
|
75
|
+
# will lead to setting the time to midnight. Suspect it is possible to end up pushing the
|
|
76
|
+
# apparent time of signature back a day.
|
|
77
|
+
if is_validating_auth:
|
|
78
|
+
map_child(approval_details, "ApprovalTimestamp", tree, f".//{{{FORM1_NS}}}SectionJ/{{{EPOC_NS}}}Signature/{{{EPOC_NS}}}Date", ns=COMMON_NS, process_func=lambda x: etsify_datetime_from_date(date.fromisoformat(x.text.split("+")[0])))
|
|
79
|
+
else:
|
|
80
|
+
map_child(approval_details, "ApprovalTimestamp", tree, f".//{{{FORM1_NS}}}SectionI/{{{EPOC_NS}}}SignatureOfAuthority/{{{EPOC_NS}}}Date", ns=COMMON_NS, process_func=lambda x: etsify_datetime_from_date(date.fromisoformat(x.text.split("+")[0])))
|
|
81
|
+
case_without_validation = tree.find(f".//{{{FORM1_NS}}}SectionI/{{{EPOC_NS}}}CaseWithoutValidation")
|
|
82
|
+
if case_without_validation is not None and case_without_validation.text.lower() == "true":
|
|
83
|
+
add_child(approval_details, "ApprovalIsEmergency", "true", ns=COMMON_NS)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# Mapping as described in table 5.3.3-5 (issuing) and 5.3.3-8 (validating)
|
|
87
|
+
def fill_in_approver_details(parent, tree, input_section, is_validating_auth):
|
|
88
|
+
approver_details = add_child(parent, "ApproverDetails", ns=COMMON_NS)
|
|
89
|
+
|
|
90
|
+
# TODO - check whether this can actually be absent (we have it as mandatory)
|
|
91
|
+
map_child(approver_details, "ApproverName", input_section, f"{{{EIO_NS}}}NameOfAuthority", ns=COMMON_NS)
|
|
92
|
+
# TODO - check whether this can actually be absent (we have it as mandatory)
|
|
93
|
+
approver_role = map_child(approver_details,
|
|
94
|
+
"ApproverRole",
|
|
95
|
+
tree,
|
|
96
|
+
f".//{{{FORM1_NS}}}SectionJ/{{{EPOC_NS}}}Type" if is_validating_auth else f".//{{{FORM1_NS}}}SectionI/{{{EPOC_NS}}}Type",
|
|
97
|
+
ns=COMMON_NS,
|
|
98
|
+
translation_func = lambda v: ROLE_MAPPING[v])
|
|
99
|
+
|
|
100
|
+
fill_in_approver_contact_details(approver_details, tree, is_alternate_poc=False, is_validating_auth=is_validating_auth)
|
|
101
|
+
if not is_validating_auth:
|
|
102
|
+
if input_section.find(f"../{{{EPOC_NS}}}ContactAuthority") is not None:
|
|
103
|
+
fill_in_approver_contact_details(approver_details, tree, is_alternate_poc=True, is_validating_auth=False)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# Mapping as described in 5.3.3.7
|
|
107
|
+
def fill_in_approver_contact_details(parent, tree, is_validating_auth, is_alternate_poc):
|
|
108
|
+
contact_details = add_child(parent, "ApproverContactDetails", ns=COMMON_NS)
|
|
109
|
+
|
|
110
|
+
if is_validating_auth:
|
|
111
|
+
input_section = tree.find(f".//{{{EPOC_NS}}}IssuingAndContactAuthority/{{{EPOC_NS}}}IssuingAuthority")
|
|
112
|
+
elif is_alternate_poc:
|
|
113
|
+
input_section = tree.find(f".//{{{EPOC_NS}}}IssuingAndContactAuthority/{{{EPOC_NS}}}ContactAuthority/{{{EPOC_NS}}}Authority")
|
|
114
|
+
else:
|
|
115
|
+
input_section = tree.find(f".//{{{FORM1_NS}}}SectionJ/{{{EPOC_NS}}}Details")
|
|
116
|
+
|
|
117
|
+
if is_alternate_poc:
|
|
118
|
+
map_child(contact_details, "Name", input_section, f"{{{EIO_NS}}}NameOfAuthority", ns=COMMON_NS)
|
|
119
|
+
add_child(contact_details, "Role", "Point of Contact", ns=COMMON_NS)
|
|
120
|
+
else:
|
|
121
|
+
map_child(contact_details, "Name", input_section, f"{{{EIO_NS}}}NameOfRepresentative", ns=COMMON_NS)
|
|
122
|
+
map_child(contact_details, "Role", input_section, f"{{{EIO_NS}}}PostHeld", ns=COMMON_NS)
|
|
123
|
+
map_child(contact_details, "EmailAddress", input_section, f"{{{EIO_NS}}}Email", ns=COMMON_NS)
|
|
124
|
+
map_child(contact_details, "PhoneNumber", input_section, f"{{{EIO_NS}}}TelNo", ns=COMMON_NS, translation_func=etisfy_tel_no)
|
|
125
|
+
map_child(contact_details, "FaxNumber", input_section, f"{{{EIO_NS}}}FaxNo", ns=COMMON_NS, translation_func=etisfy_tel_no)
|
|
126
|
+
map_child(contact_details, "Address", input_section, f"{{{EIO_NS}}}Address", ns=COMMON_NS, process_func=flatten_children)
|
|
127
|
+
if is_alternate_poc:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
# TODO - make this translation complete
|
|
131
|
+
languages = input_section.find(f".//{{{EIO_NS}}}LanguagesToCommunicate")
|
|
132
|
+
if languages is not None:
|
|
133
|
+
for language in languages.getchildren():
|
|
134
|
+
if len(language.text) > 2:
|
|
135
|
+
iso_id = LANGUAGE_MAPPING[language.text]
|
|
136
|
+
else:
|
|
137
|
+
iso_id = language.text
|
|
138
|
+
add_child(contact_details, "Languages", ns=COMMON_NS).ISO639Set1LanguageIdentifier = iso_id
|
|
139
|
+
|
|
140
|
+
# Clause 5.4.2 and table 5.3.4-1
|
|
141
|
+
def add_ld_task_object(hi1, input_xml, identifier, auth_obj_id):
|
|
142
|
+
ia_technical_id = input_xml.findall(f".//{{{EPOC_NS}}}Addressee//{{{EIO_NS}}}AuthorityTechnicalIdentifier")[0]
|
|
143
|
+
taskObj = generate_LDTaskObject(uuid_for_task(auth_obj_id, identifier.Identifier.Id.text), ia_technical_id.Country, ia_technical_id.NationalId, auth_obj_id)
|
|
144
|
+
generate_CREATE_Action(taskObj, hi1)
|
|
145
|
+
|
|
146
|
+
# This is one way of creating an LDID for each identifier (there are many others)
|
|
147
|
+
add_child(taskObj, "Reference", f"{ia_technical_id.Country}-{ia_technical_id.NationalId}-{taskObj.ObjectIdentifier}-{identifier.Identifier.Id}", ns=TASK_NS)
|
|
148
|
+
add_dict_child(taskObj, "DesiredStatus", "ETSI", "LDTaskDesiredStatus", "AwaitingDisclosure", ns=TASK_NS)
|
|
149
|
+
|
|
150
|
+
fill_in_request_details(taskObj, input_xml, identifier)
|
|
151
|
+
fill_in_delivery_details(taskObj, input_xml, identifier)
|
|
152
|
+
return taskObj
|
|
153
|
+
|
|
154
|
+
def fill_in_request_details(taskObj, input_xml, identifier):
|
|
155
|
+
request_details = add_child(taskObj, "RequestDetails", ns=TASK_NS)
|
|
156
|
+
|
|
157
|
+
subscriber_data = identifier.DataCategories.find(f"{{{EPOC_NS}}}Subscriber")
|
|
158
|
+
user_data = identifier.DataCategories.find(f"{{{EPOC_NS}}}UserIdentification")
|
|
159
|
+
traffic_data = identifier.DataCategories.find(f"{{{EPOC_NS}}}Traffic")
|
|
160
|
+
content_data = identifier.DataCategories.find(f"{{{EPOC_NS}}}Content")
|
|
161
|
+
|
|
162
|
+
if subscriber_data is not None and user_data is not None:
|
|
163
|
+
add_dict_child(request_details, "Type", "ETSI", "SubscriberDataAndUserIdentifyingData", "SubscriberData", ns=AUTH_NS)
|
|
164
|
+
elif subscriber_data is not None:
|
|
165
|
+
add_dict_child(request_details, "Type", "ETSI", "RequestType", "SubscriberData", ns=TASK_NS)
|
|
166
|
+
elif user_data is not None:
|
|
167
|
+
add_dict_child(request_details, "Type", "ETSI", "RequestType", "UserIdentifyingData", ns=TASK_NS)
|
|
168
|
+
elif content_data is not None and traffic_data is not None:
|
|
169
|
+
add_dict_child(request_details, "Type", "ETSI", "TrafficDataAndStoredContentData", "SubscriberData", ns=AUTH_NS)
|
|
170
|
+
elif content_data is not None:
|
|
171
|
+
add_dict_child(request_details, "Type", "ETSI", "RequestType", "StoredContentData", ns=TASK_NS)
|
|
172
|
+
elif traffic_data is not None:
|
|
173
|
+
add_dict_child(request_details, "Type", "ETSI", "RequestType", "TrafficData", ns=TASK_NS)
|
|
174
|
+
|
|
175
|
+
request_details.StartTime = etsify_datetime(datetime.fromisoformat(identifier.Identifier.DateTimeRange.Start.text))
|
|
176
|
+
request_details.EndTime = etsify_datetime(datetime.fromisoformat(identifier.Identifier.DateTimeRange.End.text))
|
|
177
|
+
|
|
178
|
+
# TODO - work out how to get the ObservedTimes if IP details are specified
|
|
179
|
+
fill_in_request_values(request_details, input_xml, identifier)
|
|
180
|
+
|
|
181
|
+
category_selections = identifier.DataCategories.find(f".//{{{EPOC_NS}}}Selection")
|
|
182
|
+
# TODO - check wether this is correct per the schema or not.
|
|
183
|
+
# TODO - finish this mapping properly
|
|
184
|
+
sub_types = add_child(request_details, "Subtype", ns=TASK_NS)
|
|
185
|
+
for category in category_selections:
|
|
186
|
+
add_dict_child(sub_types, "RequestSubtype", "ETSI", "EPOCRequestSubtype", category.text, ns=TASK_NS)
|
|
187
|
+
|
|
188
|
+
# TODO - come back and do TargetIdentifierSubtypes
|
|
189
|
+
|
|
190
|
+
def fill_in_request_values(request_details, input_xml, identifier):
|
|
191
|
+
request_values = add_child(request_details, "RequestValues", ns=TASK_NS)
|
|
192
|
+
request_value = add_child(request_values, "RequestValue", ns=TASK_NS)
|
|
193
|
+
|
|
194
|
+
# TODO - Come back to this as/when we know how SP-provided types are being handles
|
|
195
|
+
format_name = IDENTIFIER_TYPE_MAPPING[identifier.Identifier.Type.text]
|
|
196
|
+
format_value = identifier.Identifier.Value.text
|
|
197
|
+
target_format = add_child(request_value, "FormatType", ns=TASK_NS)
|
|
198
|
+
target_format.FormatOwner = "ETSI"
|
|
199
|
+
target_format.FormatName = format_name
|
|
200
|
+
request_value.Value = format_value
|
|
201
|
+
|
|
202
|
+
def fill_in_delivery_details(taskObj, input_xml, identifier):
|
|
203
|
+
delivery_details = add_child(taskObj, "DeliveryDetails", ns=TASK_NS)
|
|
204
|
+
dest_authorities = input_xml.findall(f".//{{{FORM1_NS}}}ToWhomTransferTheData/{{{FORM1_NS}}}AuthorityCompetences")
|
|
205
|
+
dest_authorities = set([c.text for c in dest_authorities])
|
|
206
|
+
if "ISSUING_AUTHORITY" in dest_authorities:
|
|
207
|
+
fill_in_delivery_destination(delivery_details, input_xml, input_xml.find(f".//{{{EPOC_NS}}}Addressee//{{{EIO_NS}}}AuthorityTechnicalIdentifier"), "IssuingAuthority")
|
|
208
|
+
if "VALIDATING_AUTHORITY" in dest_authorities:
|
|
209
|
+
fill_in_delivery_destination(delivery_details, input_xml, input_xml.find(f".//{{{FORM1_NS}}}SectionJ//{{{EIO_NS}}}AuthorityTechnicalIdentifier"), "ValidatingAuthority")
|
|
210
|
+
if "OTHER_COMPETENT_AUTHORITY" in dest_authorities:
|
|
211
|
+
# TODO - CCome back to this when it is clear how the techincal identifier will be transferred
|
|
212
|
+
fill_in_delivery_destination(delivery_details, input_xml, None, "OtherCompetentAuthority")
|
|
213
|
+
# TODO - come back to HandoverFormat, as this seems to just be free text
|
|
214
|
+
|
|
215
|
+
def fill_in_delivery_destination(delivery_details, input_xml, technical_id, destination_type):
|
|
216
|
+
delivery_destination = add_child(delivery_details, "LDDeliveryDestination", ns=TASK_NS)
|
|
217
|
+
if technical_id is not None:
|
|
218
|
+
delivery_address = add_child(delivery_destination, "DeliveryAddress", ns=TASK_NS)
|
|
219
|
+
endpoint_id = add_child(delivery_address, "EndpointID", ns=TASK_NS)
|
|
220
|
+
add_child(endpoint_id, "CountryCode", technical_id.Country.text, ns=CORE_NS)
|
|
221
|
+
add_child(endpoint_id, "UniqueIdentifier", technical_id.NationalId.text, ns=CORE_NS)
|
|
222
|
+
add_dict_child(delivery_destination, "LDDeliveryProfile", "ETSI", "EPOCDeliveryProfile", destination_type, ns=TASK_NS)
|
|
223
|
+
|
|
224
|
+
def add_document_object(hi1, input_xml, form_id: str):
|
|
225
|
+
ia_technical_id = input_xml.findall(f".//{{{EPOC_NS}}}Addressee//{{{EIO_NS}}}AuthorityTechnicalIdentifier")[0]
|
|
226
|
+
doc = generate_Form_Document(str(uuid4()), ia_technical_id.Country.text, ia_technical_id.NationalId.text, form_id, "EPOC/EPOC-PR Form", "Form1")
|
|
227
|
+
generate_CREATE_Action(doc, hi1)
|
|
228
|
+
|
|
229
|
+
def processForm1(input_xml, global_case_id: str, form_id: str):
|
|
230
|
+
hi1 = generate_120_request()
|
|
231
|
+
|
|
232
|
+
auth_obj = add_auth_object(hi1, input_xml, global_case_id, form_id)
|
|
233
|
+
auth_obj_id = auth_obj.ObjectIdentifier
|
|
234
|
+
|
|
235
|
+
identifiers = input_xml.findall(f".//{{{FORM1_NS}}}SectionEToF/{{{EPOC_NS}}}Evidence")
|
|
236
|
+
for identifier in identifiers:
|
|
237
|
+
add_ld_task_object(hi1, input_xml, identifier, auth_obj_id)
|
|
238
|
+
|
|
239
|
+
add_document_object(hi1, input_xml, form_id)
|
|
240
|
+
|
|
241
|
+
return hi1
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
|
|
5
|
+
from .mappings import *
|
|
6
|
+
|
|
7
|
+
from ..hi1.generators import *
|
|
8
|
+
from ..hi1.helpers import *
|
|
9
|
+
from ..xml import xml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ----------------------------------------------------------
|
|
13
|
+
# Authorisation object - clause 5.7.3
|
|
14
|
+
# ----------------------------------------------------------
|
|
15
|
+
def add_auth_object(hi1, input_xml, global_case_id, form_id):
|
|
16
|
+
ia_technical_id = input_xml.findall(f".//{{{EPOC_NS}}}Addressee//{{{EIO_NS}}}AuthorityTechnicalIdentifier")[0]
|
|
17
|
+
authObj = generate_AuthorisationObject(form_id, ia_technical_id.Country, ia_technical_id.NationalId)
|
|
18
|
+
generate_CREATE_Action(authObj, hi1)
|
|
19
|
+
|
|
20
|
+
# TODO - set authObject associated objects from Section D
|
|
21
|
+
|
|
22
|
+
add_child(authObj, "AuthorisationReference", global_case_id, ns = AUTH_NS)
|
|
23
|
+
add_dict_child(authObj, "AuthorisationLegalType", "ETSI", "EPOCLegalType", "EPOCPR", ns=AUTH_NS)
|
|
24
|
+
add_dict_child(authObj, "AuthorisationDesiredStatus", "ETSI", "AuthorisationDesiredSatus", "SubmittedToCSP", ns=AUTH_NS)
|
|
25
|
+
|
|
26
|
+
sp_concerned = input_xml.findall(f".//{{{EPOC_NS}}}Addressee//{{{EPOC_NS}}}ServiceProviderConcerned/{{{EPOC_NS}}}TechnicalIdentifier")
|
|
27
|
+
if len(sp_concerned) > 0:
|
|
28
|
+
sp_concerned = sp_concerned[0]
|
|
29
|
+
auth_cspid = add_child(authObj, "AuthorisationCSPID", ns=AUTH_NS)
|
|
30
|
+
auth_cspid = add_child(auth_cspid, "CSPID", ns=AUTH_NS)
|
|
31
|
+
add_child(auth_cspid, "CountryCode", sp_concerned[f"{{{EIO_NS}}}Country"].text, ns=CORE_NS)
|
|
32
|
+
add_child(auth_cspid, "UniqueIdentifier", sp_concerned[f"{{{EIO_NS}}}NationalId"].text, ns=CORE_NS)
|
|
33
|
+
|
|
34
|
+
fill_in_approval_details(authObj, input_xml, is_validating_auth=False)
|
|
35
|
+
if input_xml.find(f".//{{{FORM2_NS}}}SectionG") is not None:
|
|
36
|
+
fill_in_approval_details(authObj, input_xml, is_validating_auth=True)
|
|
37
|
+
|
|
38
|
+
flags = add_child(authObj, "AuthorisationFlags", ns=AUTH_NS)
|
|
39
|
+
emergency_flag = input_xml.find(f".//{{{FORM2_NS}}}SectionB/{{{EPOC_NS}}}EmergencyCase")
|
|
40
|
+
if emergency_flag is not None and emergency_flag.text.lower() == "true":
|
|
41
|
+
add_dict_child(flags, "AuthorisationFlag", "ETSI", "AuthorisationFlag", "IsEmergency", ns=AUTH_NS)
|
|
42
|
+
|
|
43
|
+
map_child(authObj, "AuthorisationLegalEntity", input_xml, f".//{{{FORM2_NS}}}SectionB/{{{EPOC_NS}}}Addressee/{{{EPOC_NS}}}Authority/{{{EIO_NS}}}NameOfAuthority", ns=AUTH_NS)
|
|
44
|
+
return authObj
|
|
45
|
+
|
|
46
|
+
# Mapping as described in table 5.3.3.5 (issuing) table 5.3.3-8 (validating)
|
|
47
|
+
def fill_in_approval_details(parent, tree, is_validating_auth):
|
|
48
|
+
approval_details = add_child(parent, "AuthorisationApprovalDetails", ns=AUTH_NS)
|
|
49
|
+
|
|
50
|
+
if is_validating_auth:
|
|
51
|
+
input_section = tree.find(f".//{{{FORM2_NS}}}SectionG/{{{EPOC_NS}}}Details")
|
|
52
|
+
else:
|
|
53
|
+
input_section = tree.find(f".//{{{EPOC_NS}}}IssuingAndContactAuthority/{{{EPOC_NS}}}IssuingAuthority")
|
|
54
|
+
|
|
55
|
+
add_child(approval_details, "ApprovalType", "ValidatingAuthority" if is_validating_auth else "IssuingAuthority", ns=COMMON_NS)
|
|
56
|
+
map_child(approval_details, "ApprovalReference", input_section, f"{{{EIO_NS}}}FileReference", ns=COMMON_NS)
|
|
57
|
+
|
|
58
|
+
fill_in_approver_details(approval_details, tree, input_section, is_validating_auth)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Mapping as described in table 5.3.3-5 (issuing) and 5.3.3-8 (validating)
|
|
62
|
+
def fill_in_approver_details(parent, tree, input_section, is_validating_auth):
|
|
63
|
+
approver_details = add_child(parent, "ApproverDetails", ns=COMMON_NS)
|
|
64
|
+
|
|
65
|
+
# TODO - check whether this can actually be absent (we have it as mandatory)
|
|
66
|
+
map_child(approver_details, "ApproverName", input_section, f"{{{EIO_NS}}}NameOfAuthority", ns=COMMON_NS)
|
|
67
|
+
# TODO - check whether this can actually be absent (we have it as mandatory)
|
|
68
|
+
approver_role = map_child(approver_details,
|
|
69
|
+
"ApproverRole",
|
|
70
|
+
tree,
|
|
71
|
+
f".//{{{FORM2_NS}}}SectionG/{{{EPOC_NS}}}Type" if is_validating_auth else f".//{{{FORM2_NS}}}SectionF/{{{EPOC_NS}}}Type",
|
|
72
|
+
ns=COMMON_NS,
|
|
73
|
+
translation_func = lambda v: ROLE_MAPPING[v])
|
|
74
|
+
|
|
75
|
+
fill_in_approver_contact_details(approver_details, tree, is_alternate_poc=False, is_validating_auth=is_validating_auth)
|
|
76
|
+
if not is_validating_auth:
|
|
77
|
+
if input_section.find(f"../{{{EPOC_NS}}}ContactAuthority") is not None:
|
|
78
|
+
fill_in_approver_contact_details(approver_details, tree, is_alternate_poc=True, is_validating_auth=False)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Mapping as described in 5.3.3.7
|
|
82
|
+
def fill_in_approver_contact_details(parent, tree, is_validating_auth, is_alternate_poc):
|
|
83
|
+
contact_details = add_child(parent, "ApproverContactDetails", ns=COMMON_NS)
|
|
84
|
+
|
|
85
|
+
if is_validating_auth:
|
|
86
|
+
input_section = tree.find(f".//{{{EPOC_NS}}}IssuingAndContactAuthority/{{{EPOC_NS}}}IssuingAuthority")
|
|
87
|
+
elif is_alternate_poc:
|
|
88
|
+
input_section = tree.find(f".//{{{EPOC_NS}}}IssuingAndContactAuthority/{{{EPOC_NS}}}ContactAuthority/{{{EPOC_NS}}}Authority")
|
|
89
|
+
else:
|
|
90
|
+
input_section = tree.find(f".//{{{FORM2_NS}}}SectionG/{{{EPOC_NS}}}Details")
|
|
91
|
+
|
|
92
|
+
if is_alternate_poc:
|
|
93
|
+
map_child(contact_details, "Name", input_section, f"{{{EIO_NS}}}NameOfAuthority", ns=COMMON_NS)
|
|
94
|
+
add_child(contact_details, "Role", "Point of Contact", ns=COMMON_NS)
|
|
95
|
+
else:
|
|
96
|
+
map_child(contact_details, "Name", input_section, f"{{{EIO_NS}}}NameOfRepresentative", ns=COMMON_NS)
|
|
97
|
+
map_child(contact_details, "Role", input_section, f"{{{EIO_NS}}}PostHeld", ns=COMMON_NS)
|
|
98
|
+
map_child(contact_details, "EmailAddress", input_section, f"{{{EIO_NS}}}Email", ns=COMMON_NS)
|
|
99
|
+
map_child(contact_details, "PhoneNumber", input_section, f"{{{EIO_NS}}}TelNo", ns=COMMON_NS, translation_func=etisfy_tel_no)
|
|
100
|
+
map_child(contact_details, "FaxNumber", input_section, f"{{{EIO_NS}}}FaxNo", ns=COMMON_NS, translation_func=etisfy_tel_no)
|
|
101
|
+
map_child(contact_details, "Address", input_section, f"{{{EIO_NS}}}Address", ns=COMMON_NS, process_func=flatten_children)
|
|
102
|
+
if is_alternate_poc:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
# TODO - make this translation complete
|
|
106
|
+
languages = input_section.find(f".//{{{EIO_NS}}}LanguagesToCommunicate")
|
|
107
|
+
if languages is not None:
|
|
108
|
+
for language in languages.getchildren():
|
|
109
|
+
if len(language.text) > 2:
|
|
110
|
+
iso_id = LANGUAGE_MAPPING[language.text]
|
|
111
|
+
else:
|
|
112
|
+
iso_id = language.text
|
|
113
|
+
add_child(contact_details, "Languages", ns=COMMON_NS).ISO639Set1LanguageIdentifier = iso_id
|
|
114
|
+
|
|
115
|
+
# Clause 5.4.2 and table 5.3.4-1
|
|
116
|
+
def add_lp_task_object(hi1, input_xml, identifier, auth_obj_id):
|
|
117
|
+
ia_technical_id = input_xml.findall(f".//{{{EPOC_NS}}}Addressee//{{{EIO_NS}}}AuthorityTechnicalIdentifier")[0]
|
|
118
|
+
taskObj = generate_LPTaskObject(uuid_for_task(auth_obj_id, identifier.Identifier.Id.text), ia_technical_id.Country, ia_technical_id.NationalId, auth_obj_id)
|
|
119
|
+
generate_CREATE_Action(taskObj, hi1)
|
|
120
|
+
|
|
121
|
+
add_dict_child(taskObj, "DesiredStatus", "ETSI", "LDTaskDesiredStatus", "AwaitingPreservation", ns=TASK_NS)
|
|
122
|
+
fill_in_request_details(taskObj, input_xml, identifier)
|
|
123
|
+
|
|
124
|
+
# TODO - this is unlikely to be the correct way to work out the expiration time. Check.
|
|
125
|
+
add_child(taskObj, "DesiredPreservationExpiration", etsify_datetime(datetime.now() + timedelta(days=60), microseconds=False), ns=TASK_NS)
|
|
126
|
+
return taskObj
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def fill_in_request_details(taskObj, input_xml, identifier):
|
|
130
|
+
request_details = add_child(taskObj, "RequestDetails", ns=TASK_NS)
|
|
131
|
+
|
|
132
|
+
subscriber_data = identifier.DataCategories.find(f"{{{EPOC_NS}}}Subscriber")
|
|
133
|
+
user_data = identifier.DataCategories.find(f"{{{EPOC_NS}}}UserIdentification")
|
|
134
|
+
traffic_data = identifier.DataCategories.find(f"{{{EPOC_NS}}}Traffic")
|
|
135
|
+
content_data = identifier.DataCategories.find(f"{{{EPOC_NS}}}Content")
|
|
136
|
+
|
|
137
|
+
if subscriber_data is not None and user_data is not None:
|
|
138
|
+
add_dict_child(request_details, "Type", "ETSI", "SubscriberDataAndUserIdentifyingData", "SubscriberData", ns=AUTH_NS)
|
|
139
|
+
elif subscriber_data is not None:
|
|
140
|
+
add_dict_child(request_details, "Type", "ETSI", "RequestType", "SubscriberData", ns=TASK_NS)
|
|
141
|
+
elif user_data is not None:
|
|
142
|
+
add_dict_child(request_details, "Type", "ETSI", "RequestType", "UserIdentifyingData", ns=TASK_NS)
|
|
143
|
+
elif content_data is not None and traffic_data is not None:
|
|
144
|
+
add_dict_child(request_details, "Type", "ETSI", "TrafficDataAndStoredContentData", "SubscriberData", ns=AUTH_NS)
|
|
145
|
+
elif content_data is not None:
|
|
146
|
+
add_dict_child(request_details, "Type", "ETSI", "RequestType", "StoredContentData", ns=TASK_NS)
|
|
147
|
+
elif traffic_data is not None:
|
|
148
|
+
add_dict_child(request_details, "Type", "ETSI", "RequestType", "TrafficData", ns=TASK_NS)
|
|
149
|
+
|
|
150
|
+
request_details.StartTime = etsify_datetime(datetime.fromisoformat(identifier.Identifier.DateTimeRange.Start.text))
|
|
151
|
+
request_details.EndTime = etsify_datetime(datetime.fromisoformat(identifier.Identifier.DateTimeRange.End.text))
|
|
152
|
+
|
|
153
|
+
# TODO - work out how to get the ObservedTimes if IP details are specified
|
|
154
|
+
fill_in_request_values(request_details, input_xml, identifier)
|
|
155
|
+
|
|
156
|
+
category_selections = identifier.DataCategories.find(f".//{{{EPOC_NS}}}Selection")
|
|
157
|
+
# TODO - check wether this is correct per the schema or not.
|
|
158
|
+
# TODO - finish this mapping properly
|
|
159
|
+
sub_types = add_child(request_details, "Subtype", ns=TASK_NS)
|
|
160
|
+
for category in category_selections:
|
|
161
|
+
add_dict_child(sub_types, "RequestSubtype", "ETSI", "EPOCRequestSubtype", category.text, ns=TASK_NS)
|
|
162
|
+
|
|
163
|
+
# TODO - come back and do TargetIdentifierSubtypes
|
|
164
|
+
|
|
165
|
+
def fill_in_request_values(request_details, input_xml, identifier):
|
|
166
|
+
request_values = add_child(request_details, "RequestValues", ns=TASK_NS)
|
|
167
|
+
request_value = add_child(request_values, "RequestValue", ns=TASK_NS)
|
|
168
|
+
|
|
169
|
+
# TODO - Come back to this as/when we know how SP-provided types are being handles
|
|
170
|
+
format_name = IDENTIFIER_TYPE_MAPPING[identifier.Identifier.Type.text]
|
|
171
|
+
format_value = identifier.Identifier.Value.text
|
|
172
|
+
target_format = add_child(request_value, "FormatType", ns=TASK_NS)
|
|
173
|
+
target_format.FormatOwner = "ETSI"
|
|
174
|
+
target_format.FormatName = format_name
|
|
175
|
+
request_value.Value = format_value
|
|
176
|
+
|
|
177
|
+
def add_document_object(hi1, input_xml, form_id: str):
|
|
178
|
+
ia_technical_id = input_xml.findall(f".//{{{EPOC_NS}}}Addressee//{{{EIO_NS}}}AuthorityTechnicalIdentifier")[0]
|
|
179
|
+
doc = generate_Form_Document(str(uuid4()), ia_technical_id.Country.text, ia_technical_id.NationalId.text, form_id, "EPOC/EPOC-PR Form", "Form2")
|
|
180
|
+
generate_CREATE_Action(doc, hi1)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def processForm2(input_xml, global_case_id: str, form_id: str):
|
|
184
|
+
hi1 = generate_120_request()
|
|
185
|
+
|
|
186
|
+
auth_obj = add_auth_object(hi1, input_xml, global_case_id, form_id)
|
|
187
|
+
auth_obj_id = auth_obj.ObjectIdentifier
|
|
188
|
+
|
|
189
|
+
identifiers = input_xml.findall(f".//{{{FORM2_NS}}}SectionC/{{{EPOC_NS}}}Evidence")
|
|
190
|
+
for identifier in identifiers:
|
|
191
|
+
add_lp_task_object(hi1, input_xml, identifier, auth_obj_id)
|
|
192
|
+
|
|
193
|
+
add_document_object(hi1, input_xml, form_id)
|
|
194
|
+
|
|
195
|
+
return hi1
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from sys import argv
|
|
5
|
+
from datetime import datetime, date
|
|
6
|
+
|
|
7
|
+
from lxml import etree, objectify
|
|
8
|
+
|
|
9
|
+
from .mappings import *
|
|
10
|
+
|
|
11
|
+
from ..hi1.generators import *
|
|
12
|
+
from ..hi1.helpers import *
|
|
13
|
+
from ..xml import xml
|
|
14
|
+
|
|
15
|
+
def fill_in_status(parent, obj_id, status_dict, status_value, details):
|
|
16
|
+
status = add_child(parent, "AssociatedObjectStatus", ns=NOTIFY_NS)
|
|
17
|
+
status.AssociatedObject = obj_id
|
|
18
|
+
add_dict_child(status, "Status", "ETSI", status_dict, status_value, ns=NOTIFY_NS)
|
|
19
|
+
status.Details = details
|
|
20
|
+
return status
|
|
21
|
+
|
|
22
|
+
def add_notification_object(hi1, input_xml, notif_id, original_auth_id):
|
|
23
|
+
sp_technical_id = input_xml.findall(f".//{{{FORM3_NS}}}SectionH//{{{EIO_NS}}}AuthorityTechnicalIdentifier")[0]
|
|
24
|
+
notif_obj = generate_NotificationObject(notif_id, sp_technical_id.Country.text, sp_technical_id.NationalId.text, original_auth_id)
|
|
25
|
+
generate_CREATE_Action(notif_obj, hi1)
|
|
26
|
+
|
|
27
|
+
add_child(notif_obj, "NotificationDetails", "Notification of Form 3", ns=NOTIFY_NS)
|
|
28
|
+
add_dict_child(notif_obj, "NotificationType", "ETSI", "EPOCNotificationType", "DeFactoImpossibility", ns=NOTIFY_NS)
|
|
29
|
+
add_child(notif_obj, "NewNotification", "true", ns=NOTIFY_NS)
|
|
30
|
+
add_child(notif_obj, "NotificationTimestamp", etsify_datetime(datetime.now(), microseconds=False), ns=NOTIFY_NS)
|
|
31
|
+
|
|
32
|
+
statuses = add_child(notif_obj, "StatusOfAssociatedObjects", ns=NOTIFY_NS)
|
|
33
|
+
|
|
34
|
+
auth_error_str = "From Form 3 Section E (as an example of what could be done)\n"
|
|
35
|
+
for elem in input_xml.find(f".//{{{FORM3_NS}}}SectionE").getchildren():
|
|
36
|
+
if elem.tag == f"{{{FORM3_NS}}}NatureOfConflictingObligations":
|
|
37
|
+
pass
|
|
38
|
+
else:
|
|
39
|
+
auth_error_str += f"{elem.tag.split("}")[1]}: {elem.text}\n"
|
|
40
|
+
|
|
41
|
+
information_required = input_xml.find(f".//{{{FORM3_NS}}}SectionF/{{{FORM3_NS}}}InformationRequiredFromIssuingAuthority")
|
|
42
|
+
if information_required is not None:
|
|
43
|
+
auth_error_str += f"From Section F\nInformationRequiredFromIssuingAuthority: {information_required.text}"
|
|
44
|
+
fill_in_status(statuses, original_auth_id, "AuthorisationStatus", "Invalid", auth_error_str)
|
|
45
|
+
|
|
46
|
+
task_map = {}
|
|
47
|
+
for identifier in input_xml.findall(f".//{{{FORM3_NS}}}SectionD/{{{FORM3_NS}}}NonExecutionReason"):
|
|
48
|
+
task_id = uuid_for_task(original_auth_id, identifier.IdentifierId)
|
|
49
|
+
task_error_str = ""
|
|
50
|
+
if hasattr(identifier, "Reasons"):
|
|
51
|
+
for reason in identifier.Reasons.getchildren():
|
|
52
|
+
task_error_str += f"Reason: {reason.text}\n"
|
|
53
|
+
if hasattr(identifier, "ExplanationOrOtherReason"):
|
|
54
|
+
task_error_str += f"ExplanationOrOtherReason:{identifier.ExplanationOrOtherReason}"
|
|
55
|
+
task_map[task_id] = task_error_str
|
|
56
|
+
for identifier in input_xml.findall(f".//{{{FORM3_NS}}}SectionG/{{{FORM3_NS}}}PreservationOfData"):
|
|
57
|
+
task_id = uuid_for_task(original_auth_id, identifier.IdentifierId)
|
|
58
|
+
task_error_str = task_map.get(task_id, "")
|
|
59
|
+
for elem in identifier.getchildren():
|
|
60
|
+
task_error_str += f"{elem.tag.split("}")[1]}: {elem.text}\n"
|
|
61
|
+
|
|
62
|
+
# TODO - Need to work out whether each task is an LPTask or an LDTask
|
|
63
|
+
for task_id, error_str in task_map.items():
|
|
64
|
+
fill_in_status(statuses, task_id, "LPTaskStatus", "Invalid", error_str)
|
|
65
|
+
return hi1
|
|
66
|
+
|
|
67
|
+
def add_document_object(hi1, input_xml, form_id: str, parent_form_id):
|
|
68
|
+
sp_technical_id = input_xml.findall(f".//{{{FORM3_NS}}}SectionH//{{{EIO_NS}}}AuthorityTechnicalIdentifier")[0]
|
|
69
|
+
doc = generate_Form_Document(str(uuid4()), sp_technical_id.Country.text, sp_technical_id.NationalId.text, parent_form_id, "EPOC/EPOC-PR Form", "Form3")
|
|
70
|
+
generate_CREATE_Action(doc, hi1)
|
|
71
|
+
|
|
72
|
+
def processForm3(input_xml, global_case_id: str, form_id: str, parent_form_id: str):
|
|
73
|
+
hi1 = generate_120_request()
|
|
74
|
+
|
|
75
|
+
add_notification_object(hi1, input_xml, form_id, parent_form_id)
|
|
76
|
+
add_document_object(hi1, input_xml, form_id, parent_form_id)
|
|
77
|
+
|
|
78
|
+
return hi1
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from sys import argv
|
|
5
|
+
from datetime import datetime, date
|
|
6
|
+
|
|
7
|
+
from lxml import etree, objectify
|
|
8
|
+
|
|
9
|
+
from .mappings import *
|
|
10
|
+
|
|
11
|
+
from ..hi1.generators import *
|
|
12
|
+
from ..hi1.helpers import *
|
|
13
|
+
from ..xml import xml
|
|
14
|
+
|
|
15
|
+
def add_lp_task_update(hi1, input_xml, identifier, auth_obj_id):
|
|
16
|
+
ia_technical_id = input_xml.findall(f".//{{{FORM5_NS}}}SectionA//{{{EIO_NS}}}AuthorityTechnicalIdentifier")[0]
|
|
17
|
+
taskObj = generate_LPTaskObject(uuid_for_task(auth_obj_id, identifier), ia_technical_id.Country, ia_technical_id.NationalId, auth_obj_id)
|
|
18
|
+
generate_UPDATE_Action(taskObj, hi1)
|
|
19
|
+
|
|
20
|
+
add_dict_child(taskObj, "DesiredStatus", "ETSI", "LDTaskDesiredStatus", "SubsequentProductionRequest", ns=TASK_NS)
|
|
21
|
+
|
|
22
|
+
return taskObj
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def add_document_object(hi1, input_xml, form_id: str, parent_form_id):
|
|
26
|
+
sp_technical_id = input_xml.findall(f".//{{{FORM5_NS}}}SectionA//{{{EIO_NS}}}AuthorityTechnicalIdentifier")[0]
|
|
27
|
+
doc = generate_Form_Document(str(uuid4()), sp_technical_id.Country.text, sp_technical_id.NationalId.text, parent_form_id, "EPOC/EPOC-PR Form", "Form5")
|
|
28
|
+
generate_CREATE_Action(doc, hi1)
|
|
29
|
+
|
|
30
|
+
def processForm5(input_xml, global_case_id: str, form_id: str, parent_form_id: str, num_of_identifiers = 1):
|
|
31
|
+
hi1 = generate_120_request()
|
|
32
|
+
|
|
33
|
+
# TODO - Form 5 doesn't contain any indication of the identifiers
|
|
34
|
+
# so the RI will need to supply the number of identifiers (in order to generate the
|
|
35
|
+
# object identifiers for the Tasks correctly)
|
|
36
|
+
for i in range(num_of_identifiers):
|
|
37
|
+
add_lp_task_update(hi1, input_xml, i+1, parent_form_id)
|
|
38
|
+
|
|
39
|
+
add_document_object(hi1, input_xml, form_id, parent_form_id)
|
|
40
|
+
|
|
41
|
+
return hi1
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from sys import argv
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
|
|
7
|
+
from lxml import etree, objectify
|
|
8
|
+
|
|
9
|
+
from .mappings import *
|
|
10
|
+
|
|
11
|
+
from ..hi1.generators import *
|
|
12
|
+
from ..hi1.helpers import *
|
|
13
|
+
from ..xml import xml
|
|
14
|
+
|
|
15
|
+
def add_lp_task_update(hi1, input_xml, identifier, auth_obj_id):
|
|
16
|
+
ia_technical_id = input_xml.findall(f".//{{{FORM6_NS}}}SectionA//{{{EIO_NS}}}AuthorityTechnicalIdentifier")[0]
|
|
17
|
+
taskObj = generate_LPTaskObject(uuid_for_task(auth_obj_id, identifier), ia_technical_id.Country, ia_technical_id.NationalId, auth_obj_id)
|
|
18
|
+
generate_UPDATE_Action(taskObj, hi1)
|
|
19
|
+
|
|
20
|
+
# TODO - this isn't the right way to calculate this date
|
|
21
|
+
# The RI will need to know the original expiration date to calculate the new one
|
|
22
|
+
# (unless we want to express this a different way)
|
|
23
|
+
add_child(taskObj, "DesiredPreservationExpiration", etsify_datetime(datetime.now() + timedelta(days=60), microseconds=False), ns=TASK_NS)
|
|
24
|
+
|
|
25
|
+
return taskObj
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def add_document_object(hi1, input_xml, form_id: str, parent_form_id):
|
|
29
|
+
sp_technical_id = input_xml.findall(f".//{{{FORM6_NS}}}SectionA//{{{EIO_NS}}}AuthorityTechnicalIdentifier")[0]
|
|
30
|
+
doc = generate_Form_Document(str(uuid4()), sp_technical_id.Country.text, sp_technical_id.NationalId.text, parent_form_id, "EPOC/EPOC-PR Form", "Form6")
|
|
31
|
+
generate_CREATE_Action(doc, hi1)
|
|
32
|
+
|
|
33
|
+
def processForm6(input_xml, global_case_id: str, form_id: str, parent_form_id: str, num_of_identifiers = 1):
|
|
34
|
+
hi1 = generate_120_request()
|
|
35
|
+
|
|
36
|
+
# TODO - Form 5 doesn't contain any indication of the identifiers
|
|
37
|
+
# so the RI will need to supply the number of identifiers (in order to generate the
|
|
38
|
+
# object identifiers for the Tasks correctly)
|
|
39
|
+
for i in range(num_of_identifiers):
|
|
40
|
+
add_lp_task_update(hi1, input_xml, i+1, parent_form_id)
|
|
41
|
+
|
|
42
|
+
add_document_object(hi1, input_xml, form_id, parent_form_id)
|
|
43
|
+
|
|
44
|
+
return hi1
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
PRIORITY_MAPPING = {
|
|
2
|
+
"AS_SOON_AS_POSSIBLE" : "TenDaysASAP",
|
|
3
|
+
"AT_THE_END_OF_TEN_DAYS" : "TenDaysSubjectToEA",
|
|
4
|
+
"IMMINENT_THREAT_TO_LIFE" : "EightHours",
|
|
5
|
+
"IMMINENT_THREAT_TO_CRITICAL_INFRASTRUCTURE" : "EighHours",
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
ROLE_MAPPING = {
|
|
9
|
+
"PUBLIC_PROSECUTOR" : "PublicProsecutor",
|
|
10
|
+
"JUDGE_COURT_OR_INVESTIGATING_JUDGE" : "JudgeCourtOrInvestigatingJudge",
|
|
11
|
+
"JUDGE" : "JudgeCourtOrInvestigatingJudge",
|
|
12
|
+
"OTHER_COMPETENT_AUTHORITY" : "OtherCompetentAuthority",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
LANGUAGE_MAPPING = {
|
|
16
|
+
"fin" : "fi",
|
|
17
|
+
"est" : "et",
|
|
18
|
+
"eng" : "en",
|
|
19
|
+
"dut" : "nl",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# TODO - work out what to do with "IP Address Range" and "IP Block"
|
|
23
|
+
|
|
24
|
+
IDENTIFIER_TYPE_MAPPING = {
|
|
25
|
+
"IPV_4" : "IPV4Address",
|
|
26
|
+
"IPV_6" : "IPV6Address",
|
|
27
|
+
"IP_ADDRESS_RANGE" : "TBD",
|
|
28
|
+
"IP_BLOCKS" : "TBD",
|
|
29
|
+
"EMAIL" : "InternationalizedEmailAddress",
|
|
30
|
+
"PHONE_NUMBER" : "InternationalE164",
|
|
31
|
+
"IMEI_NUMBER" : "IMEI",
|
|
32
|
+
"MAC_ADDRESS" : "MACAddress",
|
|
33
|
+
"USER_ID" : "ServiceAccessIdentifier",
|
|
34
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from lxml import objectify
|
|
2
|
+
from base64 import b64encode
|
|
3
|
+
|
|
4
|
+
from ..namespaces import *
|
|
5
|
+
|
|
6
|
+
def generate_120_request():
|
|
7
|
+
root = objectify.fromstring('''<HI1Message xmlns="http://uri.etsi.org/03120/common/2019/10/Core" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:common="http://uri.etsi.org/03120/common/2016/02/Common" xmlns:task="http://uri.etsi.org/03120/common/2020/09/Task" xmlns:auth="http://uri.etsi.org/03120/common/2020/09/Authorisation" xmlns:ph="http://uri.etsi.org/03120/common/2025/02/EPOCPlaceholder">
|
|
8
|
+
<Header>
|
|
9
|
+
<SenderIdentifier>
|
|
10
|
+
<CountryCode>XX</CountryCode>
|
|
11
|
+
<UniqueIdentifier>UNKNOWN</UniqueIdentifier>
|
|
12
|
+
</SenderIdentifier>
|
|
13
|
+
<ReceiverIdentifier>
|
|
14
|
+
<CountryCode>XX</CountryCode>
|
|
15
|
+
<UniqueIdentifier>UNKNOWN</UniqueIdentifier>
|
|
16
|
+
</ReceiverIdentifier>
|
|
17
|
+
<TransactionIdentifier></TransactionIdentifier>
|
|
18
|
+
<Timestamp></Timestamp>
|
|
19
|
+
<Version>
|
|
20
|
+
<ETSIVersion>V1.20.1</ETSIVersion>
|
|
21
|
+
<NationalProfileOwner>EU</NationalProfileOwner>
|
|
22
|
+
<NationalProfileVersion>v1.1.1</NationalProfileVersion>
|
|
23
|
+
</Version>
|
|
24
|
+
</Header>
|
|
25
|
+
<Payload>
|
|
26
|
+
<RequestPayload>
|
|
27
|
+
<ActionRequests/>
|
|
28
|
+
</RequestPayload>
|
|
29
|
+
</Payload>
|
|
30
|
+
</HI1Message>
|
|
31
|
+
''', parser=None)
|
|
32
|
+
return root
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def generate_CREATE_Action(new_object, hi1):
|
|
36
|
+
index = len(list(hi1.Payload.RequestPayload.ActionRequests.getchildren()))
|
|
37
|
+
root = objectify.fromstring(f'''<ActionRequest>
|
|
38
|
+
<ActionIdentifier>{index}</ActionIdentifier>
|
|
39
|
+
<CREATE>
|
|
40
|
+
</CREATE>
|
|
41
|
+
</ActionRequest>
|
|
42
|
+
''', parser=None)
|
|
43
|
+
root.CREATE.append(new_object)
|
|
44
|
+
hi1.Payload.RequestPayload.ActionRequests.append(root)
|
|
45
|
+
return root
|
|
46
|
+
|
|
47
|
+
def generate_UPDATE_Action(new_object, hi1):
|
|
48
|
+
index = len(list(hi1.Payload.RequestPayload.ActionRequests.getchildren()))
|
|
49
|
+
root = objectify.fromstring(f'''<ActionRequest>
|
|
50
|
+
<ActionIdentifier>{index}</ActionIdentifier>
|
|
51
|
+
<UPDATE>
|
|
52
|
+
</UPDATE>
|
|
53
|
+
</ActionRequest>
|
|
54
|
+
''', parser=None)
|
|
55
|
+
root.UPDATE.append(new_object)
|
|
56
|
+
hi1.Payload.RequestPayload.ActionRequests.append(root)
|
|
57
|
+
return root
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def generate_HI1Object(object_id: str, country_code: str, owner_id: str,associated_object: str | None = None, concrete_type: str | None = None):
|
|
61
|
+
root = objectify.fromstring(f'''<HI1Object xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
62
|
+
<ObjectIdentifier>{object_id}</ObjectIdentifier>
|
|
63
|
+
<CountryCode>{country_code}</CountryCode>
|
|
64
|
+
<OwnerIdentifier>{owner_id}</OwnerIdentifier>
|
|
65
|
+
</HI1Object>''', parser=None)
|
|
66
|
+
# HACK for some reason xsi:type disappears if we specify it as an attribute here
|
|
67
|
+
# Instead, we call it xsi:my_type, which doesn't disappear (for some reason)
|
|
68
|
+
# Then we do a string replacement later
|
|
69
|
+
# No idea why this is. Come back to it later.
|
|
70
|
+
if concrete_type is not None:
|
|
71
|
+
root.set(f"{{{XSI_NS}}}my_type", concrete_type)
|
|
72
|
+
if associated_object is not None:
|
|
73
|
+
root.append(objectify.fromstring(f"<AssociatedObjects><AssociatedObject>{associated_object}</AssociatedObject></AssociatedObjects>", parser=None))
|
|
74
|
+
return root
|
|
75
|
+
|
|
76
|
+
def generate_AuthorisationObject(object_id: str, country_code: str, owner_id: str, associated_object = None):
|
|
77
|
+
return generate_HI1Object(object_id, country_code, owner_id, associated_object, "auth:AuthorisationObject")
|
|
78
|
+
|
|
79
|
+
def generate_LDTaskObject(object_id: str, country_code: str, owner_id: str, associated_object = None):
|
|
80
|
+
return generate_HI1Object(object_id, country_code, owner_id, associated_object, "task:LDTaskObject")
|
|
81
|
+
|
|
82
|
+
def generate_LPTaskObject(object_id: str, country_code: str, owner_id: str, associated_object = None):
|
|
83
|
+
return generate_HI1Object(object_id, country_code, owner_id, associated_object, "task:LPTaskObject")
|
|
84
|
+
|
|
85
|
+
def generate_DocumentObject(object_id: str, country_code: str, owner_id: str, associated_object: str):
|
|
86
|
+
return generate_HI1Object(object_id, country_code, owner_id, associated_object, "doc:DocumentObject")
|
|
87
|
+
|
|
88
|
+
def generate_NotificationObject(object_id: str, country_code: str, owner_id: str, associated_object: str):
|
|
89
|
+
return generate_HI1Object(object_id, country_code, owner_id, associated_object, "notify:NotificationObject")
|
|
90
|
+
|
|
91
|
+
def generate_PlaceholderObject(object_id: str, country_code: str, owner_id: str, associated_object: str):
|
|
92
|
+
return generate_HI1Object(object_id, country_code, owner_id, associated_object, "ph:EPOCPlaceholderObject")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def generate_Form_Document(object_id: str, country_code: str, owner_id: str, associated_object: str, doc_name: str, doc_type: str):
|
|
97
|
+
doc = generate_DocumentObject(object_id, country_code, owner_id, associated_object)
|
|
98
|
+
add_child(doc, "DocumentName", doc_name, ns=DOC_NS)
|
|
99
|
+
add_dict_child(doc, "DocumentType", "ETSI", "EPOCDocumentType", doc_type, ns=DOC_NS)
|
|
100
|
+
body = add_child(doc, "DocumentBody", ns=DOC_NS)
|
|
101
|
+
contents_b64 = b64encode(doc_name.encode("utf-8"))
|
|
102
|
+
body.Contents = contents_b64
|
|
103
|
+
body.ContentType = "text/plain"
|
|
104
|
+
# TODO - come back and put a checksum in here if we want it
|
|
105
|
+
return doc
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def add_child(tree, tag_name, value = None, ns = None):
|
|
109
|
+
if value is None:
|
|
110
|
+
return add_cmplx_child(tree, tag_name, ns)
|
|
111
|
+
M = objectify.ElementMaker(namespace=ns)
|
|
112
|
+
new_element = M(tag_name, value)
|
|
113
|
+
tree.append(new_element)
|
|
114
|
+
return new_element
|
|
115
|
+
|
|
116
|
+
def add_cmplx_child(tree, tag_name, ns = None):
|
|
117
|
+
new_element = objectify.SubElement(tree, (f"{{{ns}}}" if ns is not None else "") + tag_name)
|
|
118
|
+
return new_element
|
|
119
|
+
|
|
120
|
+
def add_dict_child(tree, tag_name, dict_owner, dict_name, dict_value, ns = None):
|
|
121
|
+
new_field = add_child(tree, tag_name, ns = ns)
|
|
122
|
+
insert_DictionaryEntry(new_field, dict_owner, dict_name, dict_value)
|
|
123
|
+
return new_field
|
|
124
|
+
|
|
125
|
+
def map_child(out_tree, tag_name, in_tree, xpath, ns = None, translation_func = None, process_func = None):
|
|
126
|
+
in_element = in_tree.find(xpath)
|
|
127
|
+
if in_element is not None:
|
|
128
|
+
if process_func is not None:
|
|
129
|
+
value = process_func(in_element)
|
|
130
|
+
else:
|
|
131
|
+
value = in_element.text
|
|
132
|
+
if translation_func is not None:
|
|
133
|
+
value = translation_func(value)
|
|
134
|
+
return add_child(out_tree, tag_name, value = value, ns=ns)
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
def insert_DictionaryEntry(parent, owner: str, name: str, value: str):
|
|
138
|
+
maker = objectify.ElementMaker(namespace=COMMON_NS, nsmap=ns_map)
|
|
139
|
+
parent.append(maker.Owner(owner))
|
|
140
|
+
parent.append(maker.Name(name))
|
|
141
|
+
parent.append(maker.Value(value))
|
|
142
|
+
|
|
143
|
+
def insert_Flag(parent, flag_tag:str, owner: str, name: str, value: str):
|
|
144
|
+
newFlag = objectify.Element(flag_tag, attrib=None, nsmap=None)
|
|
145
|
+
insert_DictionaryEntry(newFlag, owner, name, value)
|
|
146
|
+
parent.append(newFlag)
|
|
147
|
+
|
|
148
|
+
def flatten_children(tree):
|
|
149
|
+
return "\n".join([c.text for c in tree.getchildren()])
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from datetime import datetime, date, time
|
|
2
|
+
from hashlib import sha256
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
def etsify_datetime(d: datetime, microseconds = True) -> str:
|
|
6
|
+
if microseconds:
|
|
7
|
+
return str(d.astimezone().isoformat())
|
|
8
|
+
else:
|
|
9
|
+
s = str(d.astimezone().isoformat())
|
|
10
|
+
return s[0:-13] + s[-6:]
|
|
11
|
+
|
|
12
|
+
def etsify_datetime_from_date(d: date) -> str:
|
|
13
|
+
return etsify_datetime(datetime.combine(d, time(0,0,0)))
|
|
14
|
+
|
|
15
|
+
# TODO - casting ISO time to date puts it in local timezone
|
|
16
|
+
# create a function that can just modify the string
|
|
17
|
+
|
|
18
|
+
def etisfy_tel_no(s: str) -> str:
|
|
19
|
+
return s.replace("(","")\
|
|
20
|
+
.replace(")","")\
|
|
21
|
+
.replace(" ","")\
|
|
22
|
+
.replace("+","")
|
|
23
|
+
|
|
24
|
+
def uuid_from_global_case_id (global_case_id: str):
|
|
25
|
+
return str(UUID(bytes = sha256(global_case_id.encode()).digest()[:16], version=4))
|
|
26
|
+
|
|
27
|
+
def uuid_for_task(auth_form_id: str, id_number: str):
|
|
28
|
+
return str(UUID(bytes = sha256(f"{auth_form_id}-{id_number}".encode()).digest()[:16], version=4))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
|
2
|
+
|
|
3
|
+
EPOC_NS = "http://data.europa.eu/edm/1/ns/epoc"
|
|
4
|
+
EIO_NS = "http://data.europa.eu/edm/1/ns/eio"
|
|
5
|
+
FORM1_NS = "http://data.europa.eu/edm/1/ns/forms/EPOC-FORM-1#"
|
|
6
|
+
FORM2_NS = "http://data.europa.eu/edm/1/ns/forms/EPOC-PR-FORM-2#"
|
|
7
|
+
FORM3_NS = "http://data.europa.eu/edm/1/ns/forms/EPOC-FORM-3#"
|
|
8
|
+
FORM5_NS = "http://data.europa.eu/edm/1/ns/forms/EPOC-PR-FORM-5#"
|
|
9
|
+
FORM6_NS = "http://data.europa.eu/edm/1/ns/forms/EPOC-PR-FORM-6#"
|
|
10
|
+
|
|
11
|
+
COMMON_NS = "http://uri.etsi.org/03120/common/2016/02/Common"
|
|
12
|
+
CORE_NS = "http://uri.etsi.org/03120/common/2019/10/Core"
|
|
13
|
+
DOC_NS = "http://uri.etsi.org/03120/common/2020/09/Document"
|
|
14
|
+
TASK_NS = "http://uri.etsi.org/03120/common/2020/09/Task"
|
|
15
|
+
AUTH_NS = "http://uri.etsi.org/03120/common/2020/09/Authorisation"
|
|
16
|
+
EF1_NS = "http://uri.etsi.org/03120/common/2025/02/EpocForm1"
|
|
17
|
+
PH_NS = "http://uri.etsi.org/03120/common/2025/02/EPOCPlaceholder"
|
|
18
|
+
NOTIFY_NS = "http://uri.etsi.org/03120/common/2016/02/Notification"
|
|
19
|
+
|
|
20
|
+
EC_AUTH_NS = "http://uri.etsi.org/03120/common/2025/02/EioAuthority"
|
|
21
|
+
|
|
22
|
+
ns_map = {
|
|
23
|
+
None : CORE_NS,
|
|
24
|
+
'auth' : AUTH_NS,
|
|
25
|
+
'doc' : DOC_NS,
|
|
26
|
+
'task' : TASK_NS,
|
|
27
|
+
'notify' : NOTIFY_NS,
|
|
28
|
+
'ef1' : EF1_NS,
|
|
29
|
+
'xsi' : XSI_NS,
|
|
30
|
+
'ph' : PH_NS,
|
|
31
|
+
|
|
32
|
+
'epoc' : EPOC_NS,
|
|
33
|
+
'eio' : EIO_NS,
|
|
34
|
+
'epocform1' : FORM1_NS,
|
|
35
|
+
'epocform2' : FORM2_NS,
|
|
36
|
+
'epocform3' : FORM3_NS,
|
|
37
|
+
'eopcform5' : FORM5_NS,
|
|
38
|
+
'eopcform6' : FORM6_NS,
|
|
39
|
+
'common' : COMMON_NS,
|
|
40
|
+
'ecauth' : EC_AUTH_NS,
|
|
41
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from lxml import objectify, etree
|
|
2
|
+
from .namespaces import *
|
|
3
|
+
|
|
4
|
+
def xml(hi1) -> str:
|
|
5
|
+
objectify.deannotate(hi1)
|
|
6
|
+
etree.cleanup_namespaces(hi1, top_nsmap=None, keep_ns_prefixes=[XSI_NS])
|
|
7
|
+
E = objectify.ElementMaker(annotate=False, namespace=CORE_NS, nsmap=ns_map)
|
|
8
|
+
result = E.HI1Message(hi1.Header, hi1.Payload)
|
|
9
|
+
etree.indent(result, space=" ")
|
|
10
|
+
xml_bytes = etree.tostring(result,
|
|
11
|
+
pretty_print=True,
|
|
12
|
+
xml_declaration=True,
|
|
13
|
+
encoding="UTF-8"
|
|
14
|
+
)
|
|
15
|
+
xml_str = xml_bytes.decode("UTF-8")
|
|
16
|
+
# HACK I don't know why lxml makes xsi:type disappear
|
|
17
|
+
xml_str = xml_str.replace("xsi:my_type", "xsi:type")
|
|
18
|
+
return xml_str
|
epoc2etsi-0.1.0/uv.lock
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 1
|
|
3
|
+
requires-python = ">=3.12"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "epoc2etsi"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
source = { editable = "." }
|
|
9
|
+
dependencies = [
|
|
10
|
+
{ name = "lxml" },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[package.metadata]
|
|
14
|
+
requires-dist = [{ name = "lxml" }]
|
|
15
|
+
|
|
16
|
+
[[package]]
|
|
17
|
+
name = "lxml"
|
|
18
|
+
version = "5.4.0"
|
|
19
|
+
source = { registry = "https://pypi.org/simple" }
|
|
20
|
+
sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479 }
|
|
21
|
+
wheels = [
|
|
22
|
+
{ url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392 },
|
|
23
|
+
{ url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103 },
|
|
24
|
+
{ url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224 },
|
|
25
|
+
{ url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913 },
|
|
26
|
+
{ url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441 },
|
|
27
|
+
{ url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165 },
|
|
28
|
+
{ url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580 },
|
|
29
|
+
{ url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493 },
|
|
30
|
+
{ url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679 },
|
|
31
|
+
{ url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691 },
|
|
32
|
+
{ url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075 },
|
|
33
|
+
{ url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680 },
|
|
34
|
+
{ url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253 },
|
|
35
|
+
{ url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651 },
|
|
36
|
+
{ url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315 },
|
|
37
|
+
{ url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149 },
|
|
38
|
+
{ url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095 },
|
|
39
|
+
{ url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086 },
|
|
40
|
+
{ url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613 },
|
|
41
|
+
{ url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008 },
|
|
42
|
+
{ url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915 },
|
|
43
|
+
{ url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890 },
|
|
44
|
+
{ url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644 },
|
|
45
|
+
{ url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817 },
|
|
46
|
+
{ url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916 },
|
|
47
|
+
{ url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274 },
|
|
48
|
+
{ url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757 },
|
|
49
|
+
{ url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028 },
|
|
50
|
+
{ url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487 },
|
|
51
|
+
{ url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688 },
|
|
52
|
+
{ url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043 },
|
|
53
|
+
{ url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569 },
|
|
54
|
+
{ url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270 },
|
|
55
|
+
{ url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606 },
|
|
56
|
+
]
|