├── .python-version
├── aiojss
├── __init__.py
├── etree
│ ├── cElementTree.py
│ ├── __init__.py
│ ├── ElementInclude.py
│ ├── ElementPath.py
│ └── ElementTree.py
└── aiojss.py
├── requirements.txt
├── .codacy.yml
├── scripts
└── Install Software Updates
│ ├── script.sh
│ └── script.xml
├── extension_attributes
└── Last User
│ ├── ea.sh
│ └── ea.xml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── tools
├── ci_tests
│ ├── validatexml.sh
│ ├── validate_files_and_folders.sh
│ └── verifyEA.py
├── hooks
│ └── pre-push
└── download.py
├── README.md
├── CODE_OF_CONDUCT.md
└── sync.py
/.python-version:
--------------------------------------------------------------------------------
1 | 3.6.3
2 |
--------------------------------------------------------------------------------
/aiojss/__init__.py:
--------------------------------------------------------------------------------
1 | from .aiojss import JSS, Script, ExtensionAttribute
2 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | aiohttp
2 | cchardet
3 | aiodns
4 | uvloop
5 | requests
6 | configparser
--------------------------------------------------------------------------------
/aiojss/etree/cElementTree.py:
--------------------------------------------------------------------------------
1 | # Deprecated alias for xml.etree.ElementTree
2 |
3 | from xml.etree.ElementTree import *
4 |
--------------------------------------------------------------------------------
/.codacy.yml:
--------------------------------------------------------------------------------
1 | ---
2 | exclude_paths:
3 | - 'aiojss/**'
4 | - 'tools/**'
5 | - 'scripts/**'
6 | - 'extension_attributes/**'
7 |
--------------------------------------------------------------------------------
/scripts/Install Software Updates/script.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Install all software updates
4 | # Example for git2jss
5 |
6 | softwareupdate -i -a
7 |
8 | exit 0
--------------------------------------------------------------------------------
/extension_attributes/Last User/ea.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | lastUser=`defaults read /Library/Preferences/com.apple.loginwindow lastUserName`
3 |
4 | if [ $lastUser == "" ]; then
5 | echo "No logins"
6 | else
7 | echo "$lastUser"
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | # Virtual environment for Python 3
4 | venv/*
5 |
6 | # Compiled Python
7 | *.pyc
8 |
9 | # Example scripts and EAs
10 | extension_attributes/*
11 | !extension_attributes/Last User/
12 | scripts/*
13 | !scripts/Install Software Updates/
14 |
--------------------------------------------------------------------------------
/scripts/Install Software Updates/script.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | When contributing to this repository, it is advised but not required to first discuss the change
4 | you wish to make via issue, email, or any other method with the owners of this repository before
5 | making a change.
6 |
7 | Please note we have a [code of conduct](https://github.com/BadStreff/git2jss/blob/master/CODE_OF_CONDUCT.md),
8 | please follow it in all your interactions with the project.
9 |
--------------------------------------------------------------------------------
/extension_attributes/Last User/ea.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Last User
4 | true
5 | This attribute displays the last user to log in. This attribute applies to both Mac and Windows.
6 | String
7 |
8 | script
9 | Mac
10 |
11 |
12 | General
13 |
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Adam Furbee
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tools/ci_tests/validatexml.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | ###################################################################################
3 | ## Validates XML for proper formatting
4 | ###################################################################################
5 |
6 | function scripts() {
7 |
8 | printf "\033[31m---------------------------------------------------------------------------------\n"
9 | printf "\033[31m Working on Scripts\n"
10 | printf "\033[31m---------------------------------------------------------------------------------\n"
11 | printf "\033[0m"
12 | scriptfolders=$(ls -ltr ./scripts | cut -c52- | awk 'NR>1')
13 | while read folder ; do
14 | echo "$folder"
15 | xmllint --noout ./scripts/"$folder"/*.xml
16 | done <<< "$scriptfolders"
17 |
18 | }
19 |
20 |
21 |
22 | function ea(){
23 | eafolders=$(ls -ltr ./extension_attributes | cut -c52- | awk 'NR>1')
24 |
25 | printf "\033[31m---------------------------------------------------------------------------------\n"
26 | printf "\033[31m Working on Extension Attributes\n"
27 | printf "\033[31m---------------------------------------------------------------------------------\n"
28 | printf "\033[0m"
29 | while read folder ; do
30 |
31 | echo "$folder"
32 |
33 | xmllint --noout ./extension_attributes/"$folder"/*.xml
34 | done <<< "$eafolders"
35 |
36 | }
37 |
38 | scripts
39 | ea
40 | exit 0
41 |
--------------------------------------------------------------------------------
/aiojss/etree/__init__.py:
--------------------------------------------------------------------------------
1 | # $Id: __init__.py 3375 2008-02-13 08:05:08Z fredrik $
2 | # elementtree package
3 |
4 | # --------------------------------------------------------------------
5 | # The ElementTree toolkit is
6 | #
7 | # Copyright (c) 1999-2008 by Fredrik Lundh
8 | #
9 | # By obtaining, using, and/or copying this software and/or its
10 | # associated documentation, you agree that you have read, understood,
11 | # and will comply with the following terms and conditions:
12 | #
13 | # Permission to use, copy, modify, and distribute this software and
14 | # its associated documentation for any purpose and without fee is
15 | # hereby granted, provided that the above copyright notice appears in
16 | # all copies, and that both that copyright notice and this permission
17 | # notice appear in supporting documentation, and that the name of
18 | # Secret Labs AB or the author not be used in advertising or publicity
19 | # pertaining to distribution of the software without specific, written
20 | # prior permission.
21 | #
22 | # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
23 | # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
24 | # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
25 | # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
26 | # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
27 | # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
28 | # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
29 | # OF THIS SOFTWARE.
30 | # --------------------------------------------------------------------
31 |
32 | # Licensed to PSF under a Contributor Agreement.
33 | # See http://www.python.org/psf/license for licensing details.
34 |
--------------------------------------------------------------------------------
/tools/ci_tests/validate_files_and_folders.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | ###################################################################################
3 | ## Looks for the existance of files in both folders
4 | ###################################################################################
5 |
6 | #Load up some variables
7 | #Define scripts and templates folders
8 | scripts=$(ls -p scripts | grep -v '/$' | sed -e 's/\..*$//')
9 | scripts_templates=$(ls -p scripts/templates/| sed -e 's/\..*$//')
10 |
11 | #Define EA and templates
12 | extensionattributes=$(ls -p extension_attributes | grep -v '/$' | sed -e 's/\..*$//')
13 | extensionattributes_templates=$(ls -p extension_attributes/templates/| sed -e 's/\..*$//')
14 |
15 |
16 | #Validate both the Script and the Template for the Script exist.
17 | echo "Making sure files exist in both places in scripts and scripts/Templates"
18 | scriptcompare=$(sdiff -bBWsw 75 <(echo "$scripts") <(echo "$scripts_templates" ))
19 |
20 | if [ "$scriptcompare" == "" ]; then
21 | echo "Script and Script Template Exist All good in the hood!"
22 | else
23 | echo "Errors! occurred please correct the below"
24 | echo " Scripts | Templates"
25 | echo "_____________________________________|______________________________"
26 | echo "$scriptcompare"
27 | echo "_____________________________________|______________________________"
28 | echo "____________"
29 | exit 1
30 | fi
31 |
32 |
33 | #Valate both the EA and the Template for the EA exist.
34 | echo "Making sure files exist in both places extension_attributes and extension_attributes/Templates"
35 | eacompare=$(sdiff -bBWsw 75 <(echo "$extensionattributes") <(echo "$extensionattributes_templates"))
36 | if [ "$eacompare" == "" ]; then
37 | echo "EA and EA Template Exist All good in the hood!"
38 | else
39 | echo "Errors! occurred please correct the below"
40 | echo " Extension Attributes | Templates"
41 | echo "_____________________________________|______________________________"
42 | echo "$eacompare"
43 | echo "_____________________________________|______________________________"
44 | exit 1
45 | fi
46 |
--------------------------------------------------------------------------------
/tools/hooks/pre-push:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # A hook script to verify what is about to be pushed. Called by "git
4 | # push" after it has checked the remote status, but before anything has been
5 | # pushed. This hook checks the keychain for jss-dev and jss-prod instances
6 | # and if they exist attempts to run the sync.py script against your dev
7 | # instance. If successful, the script will then attempt to upload to prod.
8 | # This script can and probably should be modified to suit your specific needs.
9 | # NOTE: If you are using some CI platform or there is more than 1 person
10 | # working on this repo the script should probably not be used and you should
11 | # use whatever tool your CI server provides.
12 | #
13 | # This hook is called with the following parameters:
14 | #
15 | # $1 -- Name of the remote to which the push is being done
16 | # $2 -- URL to which the push is being done
17 | #
18 | # If pushing without using a named remote those arguments will be equal.
19 | #
20 | # Information about the commits which are being pushed is supplied as lines to
21 | # the standard input in the form:
22 | #
23 | #
24 |
25 | remote="$1"
26 | url="$2"
27 |
28 |
29 | DEV_URL=`security find-generic-password -l "git2jss-dev" -g 2>&1 | grep 'svce' | cut -d \" -f 4`
30 | DEV_ACCT=`security find-generic-password -l "git2jss-dev" -g 2>&1 | grep 'acct' | cut -d \" -f 4`
31 | DEV_PASS=`security find-generic-password -l "git2jss-dev" -w`
32 |
33 | PROD_URL=`security find-generic-password -l "git2jss-dev" -g 2>&1 | grep 'svce' | cut -d \" -f 4`
34 | PROD_ACCT=`security find-generic-password -l "git2jss-dev" -g 2>&1 | grep 'acct' | cut -d \" -f 4`
35 | PROD_PASS=`security find-generic-password -l "git2jss-dev" -w`
36 |
37 | branch=$(git symbolic-ref HEAD | sed -e 's,.*/\(.*\),\1,')
38 |
39 |
40 | # Attempt the push to dev
41 | ./sync.py --url "$DEV_URL" --username "$DEV_ACCT" --password "$DEV_PASS"
42 | err=$?
43 |
44 | if [[ $err -eq 0 ]]; then
45 | echo "Push to dev successful, attempting push to prod."
46 | # but we aren't ready for prod ;)
47 | exit 0
48 | else
49 | echo "Push to dev returned an error, canceling push."
50 | echo "Remote not updated, please correct the errors before continuing"
51 | exit 1
52 | fi
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # git2jss
2 | [](https://www.codacy.com/app/adam-furbee/git2jss?utm_source=github.com&utm_medium=referral&utm_content=BadStreff/git2jss&utm_campaign=Badge_Grade)
3 |
4 | A fast asynchronous python library for syncing your scripts in git with your JSS easily. This allows admins to keep their script in a version control system for easy updating rather than googling and copy-pasting from resources that they find online.
5 |
6 | ## Getting Started
7 | 1. Fork the Project
8 | 2. Install [Python version 3.6](https://www.python.org/downloads/) or higher. (this is because of the async requirements)
9 | 3. Run `python3.6 -m pip install -r requirements.txt` to install required modules
10 | 4. Run `./tools/download.py --url https://your.jss.url:8443 --username api_user` to download all scripts and extension attributes to the repository
11 | 5. Run `./sync.py --url https://your.jss.url:8443 --username api_user` to sync all scripts back to your JSS
12 |
13 | Optional flags for `download.py`:
14 |
15 | - `--password` for CI/CD (Will prompt for password if not set)
16 | - `--do_not_verify_ssl` to skip ssl verification
17 | - `--overwrite` to overwrite all scripts and extension attributes
18 |
19 | Optional flags for `sync.py`:
20 |
21 | - `--password` for CI/CD (Will prompt for password if not set)
22 | - `--do_not_verify_ssl` to skip ssl verification
23 | - `--overwrite` to overwrite all scripts and extension attributes
24 | - `--limit` to limit max connections (default=25)
25 | - `--timeout` to limit max connections (default=60)
26 | - `--verbose` to add additional logging
27 | - `--update_all` to upload all resources in `./extension_attributes` and `./scripts`
28 | - `--jenkins` to write a Jenkins file:`jenkins.properties` with `$scripts` and `$eas` and compare `$GIT_PREVIOUS_COMMIT` with `$GIT_COMMIT`
29 |
30 | ### [ConfigParser](https://docs.python.org/3/library/configparser.html) (Optional):
31 |
32 | A config file can be created in the project root or the users home folder. When a config file exists, the script will not promt for a password.
33 |
34 | A jamfapi.cfg file can provide the following variables:
35 |
36 | - username
37 | - password
38 | - url
39 |
40 | ### Prerequisites
41 | git2jss requires [Python 3.6](https://www.python.org/downloads/) and the python modules listed in `requirements.txt`
42 |
43 | ## Deployment
44 | The project can be ran ad-hoc with the example listed above, but ideally you setup webhooks and integrate into a CI/CD pipeline so each time a push is made to the repo your scripts are re-uploaded to the JSS.
45 |
46 | ## Contributing
47 | PR's are always welcome!
48 |
--------------------------------------------------------------------------------
/tools/ci_tests/verifyEA.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import requests
4 | from xml.etree import ElementTree as ET
5 | import os
6 | import getpass
7 | import json
8 |
9 | # Use this script to validate that EA values aren't changing as a result of syncing
10 |
11 | # Overwrite computers.json
12 | overwrite = False
13 |
14 | # Constants
15 | url = 'https://your.jss.com'
16 | username = getpass.getuser()
17 | password = getpass.getpass()
18 |
19 |
20 | def overwrite_file():
21 | print('Overwriting File: computers.json...')
22 | with open('computers.json', 'w') as f:
23 | f.write(json.dumps(computers))
24 |
25 | def read_file():
26 | print('Reading cached data from disk...')
27 | with open('computers.json', 'r') as f:
28 | computers_from_disk = json.load(f)
29 | return computers_from_disk
30 |
31 | def build_computers_data_object():
32 | # Get IDs for computers
33 | print('Communicating with the Jamf Pro Server...')
34 | computers = {}
35 | r = requests.get(url + '/JSSResource/computergroups/id/810',
36 | auth = (username, password),
37 | headers= {'Content-Type': 'application/xml'})
38 |
39 | tree = ET.fromstring(r.content)
40 | resource_ids = [ e.text for e in tree.findall('computers/computer/id') ]
41 |
42 | # Download each resource and save to disk
43 | for resource_id in resource_ids:
44 |
45 | # Get detailed information about the record
46 | r = requests.get(url + '/JSSResource/computers/id/%s' % (resource_id),
47 | auth = (username, password),
48 | headers={'Content-Type': 'application/json'})
49 |
50 | # Parse xml
51 | tree = ET.fromstring(r.content)
52 | ea_values = [ e.text for e in tree.findall('extension_attributes/extension_attribute/value') ]
53 | ea_names = [ e.text for e in tree.findall('extension_attributes/extension_attribute/name') ]
54 |
55 | # Build the json for the comparison
56 | computers[resource_id] = {}
57 | for k,v in zip(ea_names,ea_values):
58 | computers[resource_id][k] = v
59 | return computers
60 |
61 |
62 | def compare_computer(computer_id):
63 | """Compares a computer id record from live to cached copy on disk
64 | params: computer_id
65 | returns: None
66 | """
67 | print("Processing Computer ID: %s" % computer_id)
68 | for key in computers[computer_id].keys():
69 | if computers[computer_id][key] != computers_from_disk[computer_id][key]:
70 | print("Value Change Found\n\tEA Name:\t{}\n\tOriginal Value:\t{}\n\tNew Value:\t{}".format(key,computers[computer_id][key],computers_from_disk[computer_id][key]))
71 |
72 | # Is this the first time it was run?
73 | mypath = os.path.dirname(os.path.realpath(__file__))
74 | if os.path.exists(os.path.join(mypath,'computers.json')):
75 | computers_from_disk = read_file()
76 | else:
77 | print('No cached data found, writing new data to computers.json')
78 | overwrite = True
79 |
80 | # Get computers information from JSS
81 | computers = build_computers_data_object()
82 |
83 | # Overwrite local file?
84 | if overwrite == True:
85 | overwrite_file()
86 |
87 | else:
88 | # Compare each computer
89 | print('Analyzing the results...')
90 | for computer_id in list(computers.keys()):
91 | compare_computer(computer_id)
92 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at adam.furbee@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/aiojss/aiojss.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=invalid-name,redefined-builtin
2 | import asyncio
3 | import aiohttp
4 |
5 | from .etree import ElementTree
6 |
7 |
8 | class NotFound(Exception):
9 | pass
10 |
11 |
12 | class JSS(object):
13 | def __init__(self, url, username, password):
14 | self.url = url
15 | self.username = username
16 | self.password = password
17 | self.auth = aiohttp.BasicAuth(username, password)
18 | self.session = aiohttp.ClientSession(loop=asyncio.get_event_loop())
19 |
20 | def __del__(self):
21 | self.session.close()
22 |
23 | async def _get_endpoint(self, endpoint, id=None, name=None):
24 | base_url = self.url + f'/JSSResource/{endpoint}'
25 | if id:
26 | url = base_url + f'/id/{id}'
27 | async with self.session.get(url, auth=self.auth) as resp:
28 | if resp.status != 200:
29 | raise NotFound
30 | return await resp.text()
31 | elif name:
32 | url = base_url + f'/name/{name}'
33 | async with self.session.get(url, auth=self.auth) as resp:
34 | if resp.status != 200:
35 | raise NotFound
36 | return await resp.text()
37 | else:
38 | async with self.session.get(base_url, auth=self.auth) as resp:
39 | if resp.status != 200:
40 | raise NotFound
41 | return await resp.text()
42 |
43 | async def _post_endpoint(self, endpoint, jss_object):
44 | base_url = self.url + f'/JSSResource/{endpoint}/name'
45 | base_url += f'/{jss_object.name.text}'
46 | headers = {'content-type': 'application/xml'}
47 | try:
48 | await self._get_endpoint(endpoint, name=jss_object.name.text)
49 | await self.session.put(base_url,
50 | auth=self.auth,
51 | data=jss_object.raw_xml(),
52 | headers=headers)
53 | except NotFound:
54 | await self.session.post(base_url,
55 | auth=self.auth,
56 | data=jss_object.raw_xml(),
57 | headers=headers)
58 |
59 | async def scripts(self, id=None, name=None):
60 | data = await self._get_endpoint('scripts', id, name)
61 | return Script(data, self)
62 |
63 | async def computer_extension_attributes(self, id=None, name=None):
64 | data = await self._get_endpoint('computerextensionattributes',
65 | id,
66 | name)
67 | return ExtensionAttribute(data, self)
68 |
69 | class JSSObject(object):
70 | def __init__(self, xml, delegate=None):
71 | self._root = ElementTree.fromstring(xml)
72 | self.delegate = delegate
73 |
74 | def __getattr__(self, attr):
75 | return self._root.__getattr__(attr)
76 |
77 | def save(self):
78 | raise NotImplementedError
79 |
80 | def delete(self):
81 | raise NotImplementedError
82 |
83 | def raw_xml(self):
84 | return ElementTree.tostring(self._root).decode("utf-8")
85 |
86 | class Script(JSSObject):
87 | def __init__(self, xml, delegate):
88 | super().__init__(xml, delegate)
89 | async def save(self):
90 | await self.delegate._post_endpoint('scripts', self)
91 | def delete(self):
92 | raise NotImplementedError
93 |
94 | class ExtensionAttribute(JSSObject):
95 | def __init__(self, xml, delegate):
96 | super().__init__(xml, delegate)
97 | async def save(self):
98 | await self.delegate._post_endpoint('computerextensionattributes',
99 | self)
100 | def delete(self):
101 | raise NotImplementedError
102 |
--------------------------------------------------------------------------------
/aiojss/etree/ElementInclude.py:
--------------------------------------------------------------------------------
1 | #
2 | # ElementTree
3 | # $Id: ElementInclude.py 3375 2008-02-13 08:05:08Z fredrik $
4 | #
5 | # limited xinclude support for element trees
6 | #
7 | # history:
8 | # 2003-08-15 fl created
9 | # 2003-11-14 fl fixed default loader
10 | #
11 | # Copyright (c) 2003-2004 by Fredrik Lundh. All rights reserved.
12 | #
13 | # fredrik@pythonware.com
14 | # http://www.pythonware.com
15 | #
16 | # --------------------------------------------------------------------
17 | # The ElementTree toolkit is
18 | #
19 | # Copyright (c) 1999-2008 by Fredrik Lundh
20 | #
21 | # By obtaining, using, and/or copying this software and/or its
22 | # associated documentation, you agree that you have read, understood,
23 | # and will comply with the following terms and conditions:
24 | #
25 | # Permission to use, copy, modify, and distribute this software and
26 | # its associated documentation for any purpose and without fee is
27 | # hereby granted, provided that the above copyright notice appears in
28 | # all copies, and that both that copyright notice and this permission
29 | # notice appear in supporting documentation, and that the name of
30 | # Secret Labs AB or the author not be used in advertising or publicity
31 | # pertaining to distribution of the software without specific, written
32 | # prior permission.
33 | #
34 | # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
35 | # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
36 | # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
37 | # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
38 | # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
39 | # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
40 | # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
41 | # OF THIS SOFTWARE.
42 | # --------------------------------------------------------------------
43 |
44 | # Licensed to PSF under a Contributor Agreement.
45 | # See http://www.python.org/psf/license for licensing details.
46 |
47 | ##
48 | # Limited XInclude support for the ElementTree package.
49 | ##
50 |
51 | import copy
52 | from . import ElementTree
53 |
54 | XINCLUDE = "{http://www.w3.org/2001/XInclude}"
55 |
56 | XINCLUDE_INCLUDE = XINCLUDE + "include"
57 | XINCLUDE_FALLBACK = XINCLUDE + "fallback"
58 |
59 | ##
60 | # Fatal include error.
61 |
62 | class FatalIncludeError(SyntaxError):
63 | pass
64 |
65 | ##
66 | # Default loader. This loader reads an included resource from disk.
67 | #
68 | # @param href Resource reference.
69 | # @param parse Parse mode. Either "xml" or "text".
70 | # @param encoding Optional text encoding (UTF-8 by default for "text").
71 | # @return The expanded resource. If the parse mode is "xml", this
72 | # is an ElementTree instance. If the parse mode is "text", this
73 | # is a Unicode string. If the loader fails, it can return None
74 | # or raise an OSError exception.
75 | # @throws OSError If the loader fails to load the resource.
76 |
77 | def default_loader(href, parse, encoding=None):
78 | if parse == "xml":
79 | with open(href, 'rb') as file:
80 | data = ElementTree.parse(file).getroot()
81 | else:
82 | if not encoding:
83 | encoding = 'UTF-8'
84 | with open(href, 'r', encoding=encoding) as file:
85 | data = file.read()
86 | return data
87 |
88 | ##
89 | # Expand XInclude directives.
90 | #
91 | # @param elem Root element.
92 | # @param loader Optional resource loader. If omitted, it defaults
93 | # to {@link default_loader}. If given, it should be a callable
94 | # that implements the same interface as default_loader.
95 | # @throws FatalIncludeError If the function fails to include a given
96 | # resource, or if the tree contains malformed XInclude elements.
97 | # @throws OSError If the function fails to load a given resource.
98 |
99 | def include(elem, loader=None):
100 | if loader is None:
101 | loader = default_loader
102 | # look for xinclude elements
103 | i = 0
104 | while i < len(elem):
105 | e = elem[i]
106 | if e.tag == XINCLUDE_INCLUDE:
107 | # process xinclude directive
108 | href = e.get("href")
109 | parse = e.get("parse", "xml")
110 | if parse == "xml":
111 | node = loader(href, parse)
112 | if node is None:
113 | raise FatalIncludeError(
114 | "cannot load %r as %r" % (href, parse)
115 | )
116 | node = copy.copy(node)
117 | if e.tail:
118 | node.tail = (node.tail or "") + e.tail
119 | elem[i] = node
120 | elif parse == "text":
121 | text = loader(href, parse, e.get("encoding"))
122 | if text is None:
123 | raise FatalIncludeError(
124 | "cannot load %r as %r" % (href, parse)
125 | )
126 | if i:
127 | node = elem[i-1]
128 | node.tail = (node.tail or "") + text + (e.tail or "")
129 | else:
130 | elem.text = (elem.text or "") + text + (e.tail or "")
131 | del elem[i]
132 | continue
133 | else:
134 | raise FatalIncludeError(
135 | "unknown parse type in xi:include tag (%r)" % parse
136 | )
137 | elif e.tag == XINCLUDE_FALLBACK:
138 | raise FatalIncludeError(
139 | "xi:fallback tag must be child of xi:include (%r)" % e.tag
140 | )
141 | else:
142 | include(e, loader)
143 | i = i + 1
144 |
--------------------------------------------------------------------------------
/tools/download.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import getpass
3 | import requests
4 | from xml.etree import ElementTree as ET
5 | from xml.dom import minidom
6 | import os
7 | import argparse
8 | import urllib3
9 | import configparser
10 |
11 | # Suppress the warning in dev
12 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
13 |
14 |
15 | # https://github.com/lazymutt/Jamf-Pro-API-Sampler/blob/5f8efa92911271248f527e70bd682db79bc600f2/jamf_duplicate_detection.py#L99
16 | def get_uapi_token():
17 | """
18 | fetches api token
19 | """
20 | jamf_test_url = url + "/api/v1/auth/token"
21 | response = requests.post(url=jamf_test_url, auth=(username, password))
22 | response_json = response.json()
23 | return response_json["token"]
24 |
25 |
26 | def invalidate_uapi_token(uapi_token):
27 | """
28 | invalidates api token
29 | """
30 | jamf_test_url = url + "/api/v1/auth/invalidate-token"
31 | headers = {"Accept": "*/*", "Authorization": "Bearer " + uapi_token}
32 | _ = requests.post(url=jamf_test_url, headers=headers)
33 |
34 |
35 | def download_scripts(
36 | mode,
37 | overwrite=None,
38 | ):
39 | """Downloads Scripts to ./scripts and Extension Attributes to ./extension_attributes
40 |
41 | Folder Structure:
42 | ./scripts/script_name/script.sh
43 | ./scripts/script_name/script.xml
44 | ./extension_attributes/ea_name/ea.sh
45 | ./extension_attributes/ea_name/ea.xml
46 |
47 | Usage:
48 |
49 | Download all Extension Attributes from JSS:
50 | download_scripts('ea','overwrite=False)
51 |
52 | Download all Extension Attributes from JSS:
53 | download_scripts('script','overwrite=False)
54 |
55 | Params:
56 | mode = 'script' or 'ea'
57 | overwrite = True/False
58 | Returns: None
59 | """
60 |
61 | # Set various values based on resource type
62 | if mode == "ea":
63 | resource = "computerextensionattributes"
64 | download_path = "extension_attributes"
65 | script_xml = "input_type/script"
66 |
67 | if mode == "script":
68 | resource = "scripts"
69 | download_path = "scripts"
70 | script_xml = "script_contents"
71 |
72 | token = get_uapi_token()
73 | # Get all IDs of resource type
74 | r = requests.get(
75 | url + "/JSSResource/%s" % resource,
76 | headers={
77 | "Accept": "application/xml",
78 | "Content-Type": "application/xml",
79 | "Authorization": "Bearer " + token,
80 | },
81 | verify=args.do_not_verify_ssl,
82 | )
83 |
84 | # Basic error handling
85 | if r.status_code != 200:
86 | print(
87 | "Something went wrong with the request, check your password and privileges and try again. \n \
88 | It's also possible that the url is incorrect. \n \
89 | Here is the HTTP Status code: %s"
90 | % r.status_code
91 | )
92 | exit(1)
93 | tree = ET.fromstring(r.content)
94 | resource_ids = [e.text for e in tree.findall(".//id")]
95 |
96 | # Download each resource and save to disk
97 | for resource_id in resource_ids:
98 | get_script = True
99 |
100 | r = requests.get(
101 | url + "/JSSResource/%s/id/%s" % (resource, resource_id),
102 | headers={
103 | "Accept": "application/xml",
104 | "Content-Type": "application/xml",
105 | "Authorization": "Bearer " + token,
106 | },
107 | verify=args.do_not_verify_ssl,
108 | )
109 | tree = ET.fromstring(r.content)
110 |
111 | if mode == "ea":
112 | if tree.find("input_type/type").text != "script":
113 | print("No script found in: %s" % tree.find("name").text)
114 | get_script = False
115 | # continue
116 |
117 | # Determine resource path (folder name)
118 | resource_path = os.path.join(export_path, download_path, tree.find("name").text)
119 |
120 | # Check to see if it exists
121 | if os.path.exists(resource_path):
122 | print("Resource is already in the repo: ", tree.find("name").text)
123 |
124 | if not overwrite:
125 | print("\tSkipping: ", tree.find("name").text)
126 | continue
127 |
128 | else: # Make the folder
129 | os.makedirs(resource_path)
130 |
131 | print("Saving: ", tree.find("name").text)
132 |
133 | # Create script string, and determine the file extension
134 | if get_script:
135 | xmlstr = ET.tostring(
136 | tree.find(script_xml), encoding="unicode", method="text"
137 | ).replace("\r", "")
138 | if xmlstr.startswith("#!/bin/sh"):
139 | ext = ".sh"
140 | elif xmlstr.startswith("#!/usr/bin/env sh"):
141 | ext = ".sh"
142 | elif xmlstr.startswith("#!/bin/bash"):
143 | ext = ".sh"
144 | elif xmlstr.startswith("#!/usr/bin/env bash"):
145 | ext = ".sh"
146 | elif xmlstr.startswith("#!/bin/zsh"):
147 | ext = ".sh"
148 | elif xmlstr.startswith("#!/usr/bin/python"):
149 | ext = ".py"
150 | elif xmlstr.startswith("#!/usr/bin/env python"):
151 | ext = ".py"
152 | elif xmlstr.startswith("#!/usr/bin/perl"):
153 | ext = ".pl"
154 | elif xmlstr.startswith("#!/usr/bin/ruby"):
155 | ext = ".rb"
156 | else:
157 | print("No interpreter directive found for: ", tree.find("name").text)
158 | ext = ".sh" # Call it sh for now so the uploader detects it
159 |
160 | with open(os.path.join(resource_path, "%s%s" % (mode, ext)), "w") as f:
161 | f.write(xmlstr)
162 |
163 | # Need to remove ID and script contents and write out xml
164 | try:
165 | tree.find(script_xml).clear()
166 | tree.remove(tree.find("id"))
167 | tree.remove(tree.find("script_contents_encoded"))
168 | tree.remove(tree.find("filename"))
169 | except:
170 | pass
171 |
172 | xmlstr = minidom.parseString(
173 | ET.tostring(tree, encoding="unicode", method="xml")
174 | ).toprettyxml(indent=" ")
175 | with open(os.path.join(resource_path, "%s.xml" % mode), "w") as f:
176 | f.write(xmlstr)
177 | invalidate_uapi_token(token)
178 |
179 |
180 | if __name__ == "__main__":
181 | # Export to current directory by default
182 | export_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")
183 |
184 | parser = argparse.ArgumentParser(description="Download Scripts from Jamf")
185 | parser.add_argument("--url")
186 | parser.add_argument("--username")
187 | parser.add_argument("--password")
188 | parser.add_argument("--export_path")
189 | parser.add_argument("--overwrite", action="store_true") # Overwrites existing files
190 | parser.add_argument(
191 | "--do_not_verify_ssl", action="store_false"
192 | ) # Skips SSL verification
193 | args = parser.parse_args()
194 | # Get configs from files
195 | CONFIG_FILE_LOCATIONS = ["jamfapi.cfg", os.path.expanduser("~/jamfapi.cfg")]
196 | CONFIG_FILE = ""
197 | # Parse Config File
198 | CONFPARSER = configparser.ConfigParser()
199 | for config_path in CONFIG_FILE_LOCATIONS:
200 | if os.path.exists(config_path):
201 | print("Found Config: {0}".format(config_path))
202 | CONFIG_FILE = config_path
203 |
204 | if CONFIG_FILE != "":
205 | try:
206 | # Get config
207 | CONFPARSER.read(CONFIG_FILE)
208 | except:
209 | print("Can't read config file")
210 | try:
211 | username = CONFPARSER.get("jss", "username")
212 | except:
213 | print("Can't find username in configfile")
214 | try:
215 | password = CONFPARSER.get("jss", "password")
216 | except:
217 | print("Can't find password in configfile")
218 | try:
219 | url = CONFPARSER.get("jss", "server")
220 | except:
221 | print("Can't find url in configfile")
222 | try:
223 | export_path = CONFPARSER.get("jss", "export_path")
224 | except:
225 | print("Can't find export_path in config")
226 |
227 | # Ask for password if not supplied via command line args
228 | if args.password:
229 | password = args.password
230 | elif password is None:
231 | password = getpass.getpass()
232 |
233 | if args.export_path:
234 | export_path = args.export_path
235 |
236 | if args.url:
237 | url = args.url
238 |
239 | if args.username:
240 | username = args.username
241 |
242 | # Run script download for extension attributes
243 | download_scripts(overwrite=args.overwrite, mode="ea")
244 | # Run script download for scripts
245 | download_scripts(overwrite=args.overwrite, mode="script")
246 |
--------------------------------------------------------------------------------
/aiojss/etree/ElementPath.py:
--------------------------------------------------------------------------------
1 | #
2 | # ElementTree
3 | # $Id: ElementPath.py 3375 2008-02-13 08:05:08Z fredrik $
4 | #
5 | # limited xpath support for element trees
6 | #
7 | # history:
8 | # 2003-05-23 fl created
9 | # 2003-05-28 fl added support for // etc
10 | # 2003-08-27 fl fixed parsing of periods in element names
11 | # 2007-09-10 fl new selection engine
12 | # 2007-09-12 fl fixed parent selector
13 | # 2007-09-13 fl added iterfind; changed findall to return a list
14 | # 2007-11-30 fl added namespaces support
15 | # 2009-10-30 fl added child element value filter
16 | #
17 | # Copyright (c) 2003-2009 by Fredrik Lundh. All rights reserved.
18 | #
19 | # fredrik@pythonware.com
20 | # http://www.pythonware.com
21 | #
22 | # --------------------------------------------------------------------
23 | # The ElementTree toolkit is
24 | #
25 | # Copyright (c) 1999-2009 by Fredrik Lundh
26 | #
27 | # By obtaining, using, and/or copying this software and/or its
28 | # associated documentation, you agree that you have read, understood,
29 | # and will comply with the following terms and conditions:
30 | #
31 | # Permission to use, copy, modify, and distribute this software and
32 | # its associated documentation for any purpose and without fee is
33 | # hereby granted, provided that the above copyright notice appears in
34 | # all copies, and that both that copyright notice and this permission
35 | # notice appear in supporting documentation, and that the name of
36 | # Secret Labs AB or the author not be used in advertising or publicity
37 | # pertaining to distribution of the software without specific, written
38 | # prior permission.
39 | #
40 | # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
41 | # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
42 | # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
43 | # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
44 | # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
45 | # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
46 | # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
47 | # OF THIS SOFTWARE.
48 | # --------------------------------------------------------------------
49 |
50 | # Licensed to PSF under a Contributor Agreement.
51 | # See http://www.python.org/psf/license for licensing details.
52 |
53 | ##
54 | # Implementation module for XPath support. There's usually no reason
55 | # to import this module directly; the ElementTree does this for
56 | # you, if needed.
57 | ##
58 |
59 | import re
60 |
61 | xpath_tokenizer_re = re.compile(
62 | r"("
63 | r"'[^']*'|\"[^\"]*\"|"
64 | r"::|"
65 | r"//?|"
66 | r"\.\.|"
67 | r"\(\)|"
68 | r"[/.*:\[\]\(\)@=])|"
69 | r"((?:\{[^}]+\})?[^/\[\]\(\)@=\s]+)|"
70 | r"\s+"
71 | )
72 |
73 | def xpath_tokenizer(pattern, namespaces=None):
74 | for token in xpath_tokenizer_re.findall(pattern):
75 | tag = token[1]
76 | if tag and tag[0] != "{" and ":" in tag:
77 | try:
78 | prefix, uri = tag.split(":", 1)
79 | if not namespaces:
80 | raise KeyError
81 | yield token[0], "{%s}%s" % (namespaces[prefix], uri)
82 | except KeyError:
83 | raise SyntaxError("prefix %r not found in prefix map" % prefix)
84 | else:
85 | yield token
86 |
87 | def get_parent_map(context):
88 | parent_map = context.parent_map
89 | if parent_map is None:
90 | context.parent_map = parent_map = {}
91 | for p in context.root.iter():
92 | for e in p:
93 | parent_map[e] = p
94 | return parent_map
95 |
96 | def prepare_child(next, token):
97 | tag = token[1]
98 | def select(context, result):
99 | for elem in result:
100 | for e in elem:
101 | if e.tag == tag:
102 | yield e
103 | return select
104 |
105 | def prepare_star(next, token):
106 | def select(context, result):
107 | for elem in result:
108 | yield from elem
109 | return select
110 |
111 | def prepare_self(next, token):
112 | def select(context, result):
113 | yield from result
114 | return select
115 |
116 | def prepare_descendant(next, token):
117 | try:
118 | token = next()
119 | except StopIteration:
120 | return
121 | if token[0] == "*":
122 | tag = "*"
123 | elif not token[0]:
124 | tag = token[1]
125 | else:
126 | raise SyntaxError("invalid descendant")
127 | def select(context, result):
128 | for elem in result:
129 | for e in elem.iter(tag):
130 | if e is not elem:
131 | yield e
132 | return select
133 |
134 | def prepare_parent(next, token):
135 | def select(context, result):
136 | # FIXME: raise error if .. is applied at toplevel?
137 | parent_map = get_parent_map(context)
138 | result_map = {}
139 | for elem in result:
140 | if elem in parent_map:
141 | parent = parent_map[elem]
142 | if parent not in result_map:
143 | result_map[parent] = None
144 | yield parent
145 | return select
146 |
147 | def prepare_predicate(next, token):
148 | # FIXME: replace with real parser!!! refs:
149 | # http://effbot.org/zone/simple-iterator-parser.htm
150 | # http://javascript.crockford.com/tdop/tdop.html
151 | signature = []
152 | predicate = []
153 | while 1:
154 | try:
155 | token = next()
156 | except StopIteration:
157 | return
158 | if token[0] == "]":
159 | break
160 | if token[0] and token[0][:1] in "'\"":
161 | token = "'", token[0][1:-1]
162 | signature.append(token[0] or "-")
163 | predicate.append(token[1])
164 | signature = "".join(signature)
165 | # use signature to determine predicate type
166 | if signature == "@-":
167 | # [@attribute] predicate
168 | key = predicate[1]
169 | def select(context, result):
170 | for elem in result:
171 | if elem.get(key) is not None:
172 | yield elem
173 | return select
174 | if signature == "@-='":
175 | # [@attribute='value']
176 | key = predicate[1]
177 | value = predicate[-1]
178 | def select(context, result):
179 | for elem in result:
180 | if elem.get(key) == value:
181 | yield elem
182 | return select
183 | if signature == "-" and not re.match(r"\-?\d+$", predicate[0]):
184 | # [tag]
185 | tag = predicate[0]
186 | def select(context, result):
187 | for elem in result:
188 | if elem.find(tag) is not None:
189 | yield elem
190 | return select
191 | if signature == "-='" and not re.match(r"\-?\d+$", predicate[0]):
192 | # [tag='value']
193 | tag = predicate[0]
194 | value = predicate[-1]
195 | def select(context, result):
196 | for elem in result:
197 | for e in elem.findall(tag):
198 | if "".join(e.itertext()) == value:
199 | yield elem
200 | break
201 | return select
202 | if signature == "-" or signature == "-()" or signature == "-()-":
203 | # [index] or [last()] or [last()-index]
204 | if signature == "-":
205 | # [index]
206 | index = int(predicate[0]) - 1
207 | if index < 0:
208 | raise SyntaxError("XPath position >= 1 expected")
209 | else:
210 | if predicate[0] != "last":
211 | raise SyntaxError("unsupported function")
212 | if signature == "-()-":
213 | try:
214 | index = int(predicate[2]) - 1
215 | except ValueError:
216 | raise SyntaxError("unsupported expression")
217 | if index > -2:
218 | raise SyntaxError("XPath offset from last() must be negative")
219 | else:
220 | index = -1
221 | def select(context, result):
222 | parent_map = get_parent_map(context)
223 | for elem in result:
224 | try:
225 | parent = parent_map[elem]
226 | # FIXME: what if the selector is "*" ?
227 | elems = list(parent.findall(elem.tag))
228 | if elems[index] is elem:
229 | yield elem
230 | except (IndexError, KeyError):
231 | pass
232 | return select
233 | raise SyntaxError("invalid predicate")
234 |
235 | ops = {
236 | "": prepare_child,
237 | "*": prepare_star,
238 | ".": prepare_self,
239 | "..": prepare_parent,
240 | "//": prepare_descendant,
241 | "[": prepare_predicate,
242 | }
243 |
244 | _cache = {}
245 |
246 | class _SelectorContext:
247 | parent_map = None
248 | def __init__(self, root):
249 | self.root = root
250 |
251 | # --------------------------------------------------------------------
252 |
253 | ##
254 | # Generate all matching objects.
255 |
256 | def iterfind(elem, path, namespaces=None):
257 | # compile selector pattern
258 | cache_key = (path, None if namespaces is None
259 | else tuple(sorted(namespaces.items())))
260 | if path[-1:] == "/":
261 | path = path + "*" # implicit all (FIXME: keep this?)
262 | try:
263 | selector = _cache[cache_key]
264 | except KeyError:
265 | if len(_cache) > 100:
266 | _cache.clear()
267 | if path[:1] == "/":
268 | raise SyntaxError("cannot use absolute path on element")
269 | next = iter(xpath_tokenizer(path, namespaces)).__next__
270 | try:
271 | token = next()
272 | except StopIteration:
273 | return
274 | selector = []
275 | while 1:
276 | try:
277 | selector.append(ops[token[0]](next, token))
278 | except StopIteration:
279 | raise SyntaxError("invalid path")
280 | try:
281 | token = next()
282 | if token[0] == "/":
283 | token = next()
284 | except StopIteration:
285 | break
286 | _cache[cache_key] = selector
287 | # execute selector pattern
288 | result = [elem]
289 | context = _SelectorContext(elem)
290 | for select in selector:
291 | result = select(context, result)
292 | return result
293 |
294 | ##
295 | # Find first matching object.
296 |
297 | def find(elem, path, namespaces=None):
298 | return next(iterfind(elem, path, namespaces), None)
299 |
300 | ##
301 | # Find all matching objects.
302 |
303 | def findall(elem, path, namespaces=None):
304 | return list(iterfind(elem, path, namespaces))
305 |
306 | ##
307 | # Find text for first matching object.
308 |
309 | def findtext(elem, path, default=None, namespaces=None):
310 | try:
311 | elem = next(iterfind(elem, path, namespaces))
312 | return elem.text or ""
313 | except StopIteration:
314 | return default
315 |
--------------------------------------------------------------------------------
/sync.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # pylint: disable=missing-docstring,invalid-name
3 | import warnings
4 | import os
5 | from os.path import dirname, join, realpath
6 | import sys
7 | import xml.etree.ElementTree as ET
8 | import getpass
9 | import argparse
10 | import logging
11 | import asyncio
12 | import async_timeout
13 | import aiohttp
14 | import uvloop
15 | import configparser
16 | import requests
17 | import configparser
18 | import requests
19 |
20 | logging.basicConfig(
21 | level=logging.DEBUG,
22 | format="%(levelname)7s: %(message)s",
23 | stream=sys.stderr,
24 | )
25 | LOG = logging.getLogger("")
26 |
27 | # The Jenkins file will contain a list of changes scripts and eas
28 | # in $scripts and $eas.
29 | # Use this variable to add a Slack emoji in front of each item if
30 | # you use a post-build action for a Slack custom message
31 | SLACK_EMOJI = ":white_check_mark: "
32 | SUPPORTED_SCRIPT_EXTENSIONS = ("sh", "py", "pl", "swift", "rb")
33 | SUPPORTED_EA_EXTENSIONS = ("sh", "py", "pl", "swift", "rb")
34 | CATEGORIES = []
35 |
36 |
37 | # https://github.com/lazymutt/Jamf-Pro-API-Sampler/blob/5f8efa92911271248f527e70bd682db79bc600f2/jamf_duplicate_detection.py#L99
38 | def get_uapi_token():
39 | """
40 | fetches api token
41 | """
42 | jamf_test_url = url + "/api/v1/auth/token"
43 | response = requests.post(url=jamf_test_url, auth=(username, password))
44 | response_json = response.json()
45 | return response_json["token"]
46 |
47 |
48 | def invalidate_uapi_token(uapi_token):
49 | """
50 | invalidates api token
51 | """
52 | jamf_test_url = url + "/api/v1/auth/invalidate-token"
53 | headers = {"Accept": "*/*", "Authorization": "Bearer " + uapi_token}
54 | _ = requests.post(url=jamf_test_url, headers=headers)
55 |
56 |
57 | # https://github.com/lazymutt/Jamf-Pro-API-Sampler/blob/5f8efa92911271248f527e70bd682db79bc600f2/jamf_duplicate_detection.py#L99
58 | def get_uapi_token():
59 | """
60 | fetches api token
61 | """
62 | jamf_test_url = url + "/api/v1/auth/token"
63 | response = requests.post(url=jamf_test_url, auth=(username, password))
64 | response_json = response.json()
65 | return response_json["token"]
66 |
67 |
68 | def invalidate_uapi_token(uapi_token):
69 | """
70 | invalidates api token
71 | """
72 | jamf_test_url = url + "/api/v1/auth/invalidate-token"
73 | headers = {"Accept": "*/*", "Authorization": "Bearer " + uapi_token}
74 | _ = requests.post(url=jamf_test_url, headers=headers)
75 |
76 |
77 | def check_for_changes():
78 | """Looks for files that were changed between the current commit and
79 | the last commit so we don't upload everything on every run
80 | --jenkins will utilize $GIT_PREVIOUS_COMMIT and $GIT_COMMIT
81 | environmental variables
82 | --update_all can be invoked to upload all scripts and
83 | extension attributes
84 | """
85 | # This line will work with the environmental variables in Jenkins
86 | if args.jenkins:
87 | git_changes = (
88 | os.popen("git diff --name-only $GIT_PREVIOUS_COMMIT $GIT_COMMIT")
89 | .read()
90 | .split("\n")
91 | )
92 |
93 | # Compare the last two commits to determine the list of files that
94 | # were changed
95 | else:
96 | git_commits = (
97 | os.popen('git log -2 --pretty=oneline --pretty=format:"%h"')
98 | .read()
99 | .split("\n")
100 | )
101 | command = "git diff --name-only" + " " + git_commits[1] + " " + git_commits[0]
102 | git_changes = os.popen(command).read().split("\n")
103 |
104 | for i in git_changes:
105 | if "extension_attributes/" in i and i.split("/")[1] not in changed_ext_attrs:
106 | changed_ext_attrs.append(i.split("/")[1])
107 |
108 | for i in git_changes:
109 | if "scripts/" in i and i.split("/")[1] not in changed_scripts:
110 | changed_scripts.append(i.split("/")[1])
111 |
112 |
113 | def write_jenkins_file():
114 | """Write changed_ext_attrs and changed_scripts to jenkins file.
115 | $eas will contains the changed extension attributes,
116 | $scripts will contains the changed scripts
117 | If there are no changes, the variable will be set to 'None'
118 | """
119 |
120 | if not changed_ext_attrs:
121 | contents = "eas=" + "None"
122 | else:
123 | contents = "eas=" + SLACK_EMOJI + changed_ext_attrs[0] + "\\n" + "\\"
124 | for changed_ext_attr in changed_ext_attrs[1:]:
125 | contents = contents + "\n" + SLACK_EMOJI + changed_ext_attr + "\\n" + "\\"
126 |
127 | if not changed_scripts:
128 | contents = contents.rstrip("\\") + "\n" + "scripts=" + "None"
129 |
130 | else:
131 | contents = (
132 | contents.rstrip("\\")
133 | + "\n"
134 | + "scripts="
135 | + SLACK_EMOJI
136 | + changed_scripts[0]
137 | + "\\n"
138 | + "\\"
139 | )
140 | for changed_script in changed_scripts[1:]:
141 | contents = contents + "\n" + SLACK_EMOJI + changed_script + "\\n" + "\\"
142 |
143 | with open("jenkins.properties", "w") as f:
144 | f.write(contents)
145 |
146 |
147 | async def upload_extension_attributes(session, url, user, passwd, semaphore):
148 | # sync_path = dirname(realpath(__file__))
149 | if not changed_ext_attrs and not args.update_all:
150 | print("No Changes in Extension Attributes")
151 | return
152 | ext_attrs = [
153 | f.name
154 | for f in os.scandir(join(sync_path, "extension_attributes"))
155 | if f.is_dir() and f.name in changed_ext_attrs
156 | ]
157 | if args.update_all:
158 | print("Copying all extension attributes...")
159 | ext_attrs = [
160 | f.name
161 | for f in os.scandir(join(sync_path, "extension_attributes"))
162 | if f.is_dir()
163 | ]
164 | tasks = []
165 | for ea in ext_attrs:
166 | task = asyncio.ensure_future(
167 | upload_extension_attribute(session, url, user, passwd, ea, semaphore)
168 | )
169 | tasks.append(task)
170 | await asyncio.gather(*tasks)
171 |
172 |
173 | async def upload_extension_attribute(session, url, user, passwd, ext_attr, semaphore):
174 | has_script = True
175 |
176 | # sync_path = dirname(realpath(__file__))
177 | # auth = aiohttp.BasicAuth(user, passwd)
178 | headers = {
179 | "Accept": "application/xml",
180 | "Content-Type": "application/xml",
181 | "Authorization": "Bearer " + token,
182 | }
183 | # Get the script files within the folder, we'll only use
184 | # script_file[0] in case there are multiple files
185 | script_file = [
186 | f.name
187 | for f in os.scandir(join(sync_path, "extension_attributes", ext_attr))
188 | if f.is_file() and f.name.split(".")[-1] in SUPPORTED_EA_EXTENSIONS
189 | ]
190 | if script_file == []:
191 | print("Warning: No script file found in extension_attributes/%s" % ext_attr)
192 | has_script = False
193 | # return # Need to skip if no script.
194 | if has_script:
195 | with open(
196 | join(sync_path, "extension_attributes", ext_attr, script_file[0]), "r"
197 | ) as f:
198 | data = f.read()
199 | async with semaphore:
200 | with async_timeout.timeout(args.timeout):
201 | template = await get_ea_template(session, url, user, passwd, ext_attr)
202 | async with session.get(
203 | url
204 | + "/JSSResource/computerextensionattributes/name/"
205 | + template.find("name").text,
206 | headers=headers,
207 | ) as resp:
208 | if has_script and data:
209 | template.find("input_type/script").text = data
210 | if args.verbose:
211 | print(ET.tostring(template))
212 | print("response status initial get: ", resp.status)
213 | if resp.status == 200:
214 | put_url = (
215 | url
216 | + "/JSSResource/computerextensionattributes/name/"
217 | + template.find("name").text
218 | )
219 | resp = await session.put(
220 | put_url, data=ET.tostring(template), headers=headers
221 | )
222 | else:
223 | post_url = url + "/JSSResource/computerextensionattributes/id/0"
224 | resp = await session.post(
225 | post_url, data=ET.tostring(template), headers=headers
226 | )
227 | if args.verbose:
228 | print("response status: ", resp.status)
229 | print("EA: ", ext_attr)
230 | print("EA Name: ", template.find("name").text)
231 | if resp.status in (201, 200):
232 | print("Uploaded Extension Attribute: %s" % template.find("name").text)
233 | else:
234 | print("Error uploading script: %s" % template.find("name").text)
235 | print("Error: %s" % resp.status)
236 | return resp.status
237 |
238 |
239 | async def get_ea_template(session, url, user, passwd, ext_attr):
240 | # auth = aiohttp.BasicAuth(user, passwd)
241 | # sync_path = dirname(realpath(__file__))
242 | xml_file = [
243 | f.name
244 | for f in os.scandir(join(sync_path, "extension_attributes", ext_attr))
245 | if f.is_file() and f.name.split(".")[-1] in "xml"
246 | ]
247 | try:
248 | with open(
249 | join(sync_path, "extension_attributes", ext_attr, xml_file[0]), "r"
250 | ) as file:
251 | template = ET.fromstring(file.read())
252 | except IndexError:
253 | with async_timeout.timeout(args.timeout):
254 | headers = {
255 | "Accept": "application/xml",
256 | "Content-Type": "application/xml",
257 | "Authorization": "Bearer " + token,
258 | }
259 |
260 | async with session.get(
261 | url + "/JSSResource/computerextensionattributes/name/" + ext_attr,
262 | headers=headers,
263 | ) as resp:
264 | if resp.status == 200:
265 | async with session.get(
266 | url
267 | + "/JSSResource/computerextensionattributes/name/"
268 | + ext_attr,
269 | headers=headers,
270 | ) as response:
271 | template = ET.fromstring(await response.text())
272 | else:
273 | template = ET.parse(join(sync_path, "templates/ea.xml")).getroot()
274 | # name is mandatory, so we use the foldername if nothing is set in
275 | # a template
276 | if args.verbose:
277 | print(ET.tostring(template))
278 | if template.find("category") and template.find("category").text not in CATEGORIES:
279 | ET.SubElement(template, "category").text = "None"
280 | if args.verbose:
281 | c = template.find("category").text
282 | print(
283 | f"""WARNING: Unable to find category {c} in the JSS,
284 | setting to None"""
285 | )
286 | if template.find("name") is None:
287 | ET.SubElement(template, "name").text = ext_attr
288 | elif not template.find("name").text or template.find("name").text is None:
289 | template.find("name").text = ext_attr
290 | return template
291 |
292 |
293 | async def upload_scripts(session, url, user, passwd, semaphore):
294 | # sync_path = dirname(realpath(__file__))
295 |
296 | if not changed_scripts and not args.update_all:
297 | print("No Changes in Scripts")
298 | scripts = [
299 | f.name
300 | for f in os.scandir(join(sync_path, "scripts"))
301 | if f.is_dir() and f.name in changed_scripts
302 | ]
303 | if args.update_all:
304 | print("Copying all scripts...")
305 | scripts = [f.name for f in os.scandir(join(sync_path, "scripts")) if f.is_dir()]
306 |
307 | tasks = []
308 | for script in scripts:
309 | task = asyncio.ensure_future(
310 | upload_script(session, url, user, passwd, script, semaphore)
311 | )
312 | tasks.append(task)
313 | await asyncio.gather(*tasks)
314 |
315 |
316 | async def upload_script(session, url, user, passwd, script, semaphore):
317 | # sync_path = dirname(realpath(__file__))
318 | # auth = aiohttp.BasicAuth(user, passwd)
319 | headers = {
320 | "Accept": "application/xml",
321 | "Content-Type": "application/xml",
322 | "Authorization": "Bearer " + token,
323 | }
324 | script_file = [
325 | f.name
326 | for f in os.scandir(join(sync_path, "scripts", script))
327 | if f.is_file() and f.name.split(".")[-1] in SUPPORTED_SCRIPT_EXTENSIONS
328 | ]
329 | if script_file == []:
330 | print("Warning: No script file found in scripts/%s" % script)
331 | return # Need to skip if no script.
332 | with open(join(sync_path, "scripts", script, script_file[0]), "r") as f:
333 | data = f.read()
334 | async with semaphore:
335 | with async_timeout.timeout(args.timeout):
336 | template = await get_script_template(session, url, user, passwd, script)
337 | async with session.get(
338 | url + "/JSSResource/scripts/name/" + template.find("name").text,
339 | headers=headers,
340 | ) as resp:
341 | template.find("script_contents").text = data
342 | if resp.status == 200:
343 | put_url = (
344 | url + "/JSSResource/scripts/name/" + template.find("name").text
345 | )
346 | resp = await session.put(
347 | put_url, data=ET.tostring(template), headers=headers
348 | )
349 | else:
350 | post_url = url + "/JSSResource/scripts/id/0"
351 | resp = await session.post(
352 | post_url, data=ET.tostring(template), headers=headers
353 | )
354 | if resp.status in (201, 200):
355 | print("Uploaded script: %s" % template.find("name").text)
356 | else:
357 | print("Error uploading script: %s" % template.find("name").text)
358 | print("Error: %s" % resp.status)
359 | return resp.status
360 |
361 |
362 | async def get_script_template(session, url, user, passwd, script):
363 | # auth = aiohttp.BasicAuth(user, passwd)
364 | # sync_path = dirname(realpath(__file__))
365 | xml_file = [
366 | f.name
367 | for f in os.scandir(join(sync_path, "scripts", script))
368 | if f.is_file() and f.name.split(".")[-1] in "xml"
369 | ]
370 | try:
371 | with open(join(sync_path, "scripts", script, xml_file[0]), "r") as file:
372 | template = ET.fromstring(file.read())
373 | except IndexError:
374 | with async_timeout.timeout(args.timeout):
375 | headers = {
376 | "Accept": "application/xml",
377 | "Content-Type": "application/xml",
378 | "Authorization": "Bearer " + token,
379 | }
380 | async with session.get(
381 | url + "/JSSResource/scripts/name/" + script, headers=headers
382 | ) as resp:
383 | if resp.status == 200:
384 | async with session.get(
385 | url + "/JSSResource/scripts/name/" + script, headers=headers
386 | ) as response:
387 | template = ET.fromstring(await response.text())
388 | else:
389 | template = ET.parse(
390 | join(sync_path, "templates/script.xml")
391 | ).getroot()
392 | # name is mandatory, so we use the filename if nothing is set in a template
393 | if args.verbose:
394 | print(ET.tostring(template))
395 | if (
396 | template.find("category") is not None
397 | and template.find("category").text not in CATEGORIES
398 | ):
399 | c = template.find("category").text
400 | template.remove(template.find("category"))
401 | if args.verbose:
402 | print(
403 | f"""WARNING: Unable to find category "{c}" in the JSS,
404 | setting to None"""
405 | )
406 | if template.find("name") is None:
407 | ET.SubElement(template, "name").text = script
408 | elif not template.find("name").text or template.find("name").text is None:
409 | template.find("name").text = script
410 | return template
411 |
412 |
413 | async def get_existing_categories(session, url, user, passwd, semaphore):
414 | # auth = aiohttp.BasicAuth(user, passwd)
415 | headers = {
416 | "Accept": "application/xml",
417 | "Content-Type": "application/xml",
418 | "Authorization": "Bearer " + token,
419 | }
420 | async with semaphore:
421 | with async_timeout.timeout(args.timeout):
422 | async with session.get(
423 | url + "/JSSResource/categories", headers=headers
424 | ) as resp:
425 | if resp.status in (201, 200):
426 | return [
427 | c.find("name").text
428 | for c in [
429 | e
430 | for e in ET.fromstring(await resp.text()).findall(
431 | "category"
432 | )
433 | ]
434 | ]
435 | return []
436 |
437 |
438 | async def main():
439 | # pylint: disable=global-statement
440 | global CATEGORIES
441 | semaphore = asyncio.BoundedSemaphore(args.limit)
442 | async with aiohttp.ClientSession() as session:
443 | async with aiohttp.ClientSession(
444 | connector=aiohttp.TCPConnector(ssl=args.do_not_verify_ssl)
445 | ) as session:
446 | CATEGORIES = await get_existing_categories(
447 | session, url, username, password, semaphore
448 | )
449 | await upload_scripts(session, url, username, password, semaphore)
450 | await upload_extension_attributes(
451 | session, url, username, password, semaphore
452 | )
453 |
454 |
455 | if __name__ == "__main__":
456 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
457 |
458 | # Export to current directory by default
459 | sync_path = dirname(realpath(__file__))
460 |
461 | parser = argparse.ArgumentParser(description="Sync repo with JamfPro")
462 | parser.add_argument("--url")
463 | parser.add_argument("--username")
464 | parser.add_argument("--password")
465 | parser.add_argument("--sync_path")
466 | parser.add_argument("--limit", type=int, default=25)
467 | parser.add_argument("--timeout", type=int, default=60)
468 | parser.add_argument("--verbose", action="store_true")
469 | parser.add_argument("--do_not_verify_ssl", action="store_false")
470 | parser.add_argument("--update_all", action="store_true")
471 | parser.add_argument("--jenkins", action="store_true")
472 | args = parser.parse_args()
473 |
474 | changed_ext_attrs = []
475 | changed_scripts = []
476 | check_for_changes()
477 | print("Changed Extension Attributes: ", changed_ext_attrs)
478 | print("Changed Scripts: ", changed_scripts)
479 |
480 | if args.jenkins:
481 | write_jenkins_file()
482 | # Set configs file locations
483 | CONFIG_FILE_LOCATIONS = ["jamfapi.cfg", os.path.expanduser("~/jamfapi.cfg")]
484 | CONFIG_FILE = ""
485 | # Parse Config File
486 | CONFPARSER = configparser.ConfigParser()
487 | for config_path in CONFIG_FILE_LOCATIONS:
488 | if os.path.exists(config_path):
489 | print("Found Config: {0}".format(config_path))
490 | CONFIG_FILE = config_path
491 |
492 | if CONFIG_FILE != "":
493 | try:
494 | # Get config
495 | CONFPARSER.read(CONFIG_FILE)
496 | except:
497 | print("Can't read config file")
498 | try:
499 | username = CONFPARSER.get("jss", "username")
500 | except:
501 | print("Can't find username in configfile")
502 | try:
503 | password = CONFPARSER.get("jss", "password")
504 | except:
505 | print("Can't find password in configfile")
506 | try:
507 | url = CONFPARSER.get("jss", "server")
508 | except:
509 | print("Can't find url in configfile")
510 | try:
511 | sync_path = CONFPARSER.get("jss", "sync_path")
512 | except:
513 | print("Can't find sync_path in config")
514 |
515 | # Ask for password if not supplied via command line args
516 | if args.password:
517 | password = args.password
518 | elif password is None:
519 | password = getpass.getpass()
520 |
521 | if args.sync_path:
522 | sync_path = args.sync_path
523 |
524 | if args.url:
525 | url = args.url
526 |
527 | if args.username:
528 | username = args.username
529 |
530 | token = get_uapi_token()
531 |
532 | loop = asyncio.get_event_loop()
533 |
534 | if args.verbose:
535 | loop.set_debug(True)
536 | loop.slow_callback_duration = 0.001
537 | warnings.simplefilter("always", ResourceWarning)
538 |
539 | loop.run_until_complete(main())
540 |
--------------------------------------------------------------------------------
/aiojss/etree/ElementTree.py:
--------------------------------------------------------------------------------
1 | """Lightweight XML support for Python.
2 |
3 | XML is an inherently hierarchical data format, and the most natural way to
4 | represent it is with a tree. This module has two classes for this purpose:
5 |
6 | 1. ElementTree represents the whole XML document as a tree and
7 |
8 | 2. Element represents a single node in this tree.
9 |
10 | Interactions with the whole document (reading and writing to/from files) are
11 | usually done on the ElementTree level. Interactions with a single XML element
12 | and its sub-elements are done on the Element level.
13 |
14 | Element is a flexible container object designed to store hierarchical data
15 | structures in memory. It can be described as a cross between a list and a
16 | dictionary. Each Element has a number of properties associated with it:
17 |
18 | 'tag' - a string containing the element's name.
19 |
20 | 'attributes' - a Python dictionary storing the element's attributes.
21 |
22 | 'text' - a string containing the element's text content.
23 |
24 | 'tail' - an optional string containing text after the element's end tag.
25 |
26 | And a number of child elements stored in a Python sequence.
27 |
28 | To create an element instance, use the Element constructor,
29 | or the SubElement factory function.
30 |
31 | You can also use the ElementTree class to wrap an element structure
32 | and convert it to and from XML.
33 |
34 | """
35 |
36 | #---------------------------------------------------------------------
37 | # Licensed to PSF under a Contributor Agreement.
38 | # See http://www.python.org/psf/license for licensing details.
39 | #
40 | # ElementTree
41 | # Copyright (c) 1999-2008 by Fredrik Lundh. All rights reserved.
42 | #
43 | # fredrik@pythonware.com
44 | # http://www.pythonware.com
45 | # --------------------------------------------------------------------
46 | # The ElementTree toolkit is
47 | #
48 | # Copyright (c) 1999-2008 by Fredrik Lundh
49 | #
50 | # By obtaining, using, and/or copying this software and/or its
51 | # associated documentation, you agree that you have read, understood,
52 | # and will comply with the following terms and conditions:
53 | #
54 | # Permission to use, copy, modify, and distribute this software and
55 | # its associated documentation for any purpose and without fee is
56 | # hereby granted, provided that the above copyright notice appears in
57 | # all copies, and that both that copyright notice and this permission
58 | # notice appear in supporting documentation, and that the name of
59 | # Secret Labs AB or the author not be used in advertising or publicity
60 | # pertaining to distribution of the software without specific, written
61 | # prior permission.
62 | #
63 | # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
64 | # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
65 | # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
66 | # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
67 | # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
68 | # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
69 | # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
70 | # OF THIS SOFTWARE.
71 | # --------------------------------------------------------------------
72 |
73 | __all__ = [
74 | # public symbols
75 | "Comment",
76 | "dump",
77 | "Element", "ElementTree",
78 | "fromstring", "fromstringlist",
79 | "iselement", "iterparse",
80 | "parse", "ParseError",
81 | "PI", "ProcessingInstruction",
82 | "QName",
83 | "SubElement",
84 | "tostring", "tostringlist",
85 | "TreeBuilder",
86 | "VERSION",
87 | "XML", "XMLID",
88 | "XMLParser", "XMLPullParser",
89 | "register_namespace",
90 | ]
91 |
92 | VERSION = "1.3.0"
93 |
94 | import sys
95 | import re
96 | import warnings
97 | import io
98 | import collections
99 | import contextlib
100 |
101 | from . import ElementPath
102 |
103 |
104 | class ParseError(SyntaxError):
105 | """An error when parsing an XML document.
106 |
107 | In addition to its exception value, a ParseError contains
108 | two extra attributes:
109 | 'code' - the specific exception code
110 | 'position' - the line and column of the error
111 |
112 | """
113 | pass
114 |
115 | # --------------------------------------------------------------------
116 |
117 |
118 | def iselement(element):
119 | """Return True if *element* appears to be an Element."""
120 | return hasattr(element, 'tag')
121 |
122 |
123 | class Element:
124 | """An XML element.
125 |
126 | This class is the reference implementation of the Element interface.
127 |
128 | An element's length is its number of subelements. That means if you
129 | want to check if an element is truly empty, you should check BOTH
130 | its length AND its text attribute.
131 |
132 | The element tag, attribute names, and attribute values can be either
133 | bytes or strings.
134 |
135 | *tag* is the element name. *attrib* is an optional dictionary containing
136 | element attributes. *extra* are additional element attributes given as
137 | keyword arguments.
138 |
139 | Example form:
140 | text...tail
141 |
142 | """
143 |
144 | tag = None
145 | """The element's name."""
146 |
147 | attrib = None
148 | """Dictionary of the element's attributes."""
149 |
150 | text = None
151 | """
152 | Text before first subelement. This is either a string or the value None.
153 | Note that if there is no text, this attribute may be either
154 | None or the empty string, depending on the parser.
155 |
156 | """
157 |
158 | tail = None
159 | """
160 | Text after this element's end tag, but before the next sibling element's
161 | start tag. This is either a string or the value None. Note that if there
162 | was no text, this attribute may be either None or an empty string,
163 | depending on the parser.
164 |
165 | """
166 |
167 | def __init__(self, tag, attrib={}, **extra):
168 | if not isinstance(attrib, dict):
169 | raise TypeError("attrib must be dict, not %s" % (
170 | attrib.__class__.__name__,))
171 | attrib = attrib.copy()
172 | attrib.update(extra)
173 | self.tag = tag
174 | self.attrib = attrib
175 | self._children = []
176 |
177 | def __repr__(self):
178 | return "<%s %r at %#x>" % (self.__class__.__name__, self.tag, id(self))
179 |
180 | def makeelement(self, tag, attrib):
181 | """Create a new element with the same type.
182 |
183 | *tag* is a string containing the element name.
184 | *attrib* is a dictionary containing the element attributes.
185 |
186 | Do not call this method, use the SubElement factory function instead.
187 |
188 | """
189 | return self.__class__(tag, attrib)
190 |
191 | def copy(self):
192 | """Return copy of current element.
193 |
194 | This creates a shallow copy. Subelements will be shared with the
195 | original tree.
196 |
197 | """
198 | elem = self.makeelement(self.tag, self.attrib)
199 | elem.text = self.text
200 | elem.tail = self.tail
201 | elem[:] = self
202 | return elem
203 |
204 | def __len__(self):
205 | return len(self._children)
206 |
207 | def __bool__(self):
208 | warnings.warn(
209 | "The behavior of this method will change in future versions. "
210 | "Use specific 'len(elem)' or 'elem is not None' test instead.",
211 | FutureWarning, stacklevel=2
212 | )
213 | return len(self._children) != 0 # emulate old behaviour, for now
214 |
215 | def __getitem__(self, index):
216 | return self._children[index]
217 |
218 | def __setitem__(self, index, element):
219 | # if isinstance(index, slice):
220 | # for elt in element:
221 | # assert iselement(elt)
222 | # else:
223 | # assert iselement(element)
224 | self._children[index] = element
225 |
226 | def __getattr__(self, attr):
227 | # print(f'calling Element.__getattr__(self, {attr})')
228 | r = []
229 | for e in self._children:
230 | if e.tag == attr:
231 | r.append(e)
232 | if r == []:
233 | raise AttributeError
234 | elif len(r) == 1:
235 | return r[0]
236 | else:
237 | return r
238 |
239 | def __delitem__(self, index):
240 | del self._children[index]
241 |
242 | def append(self, subelement):
243 | """Add *subelement* to the end of this element.
244 |
245 | The new element will appear in document order after the last existing
246 | subelement (or directly after the text, if it's the first subelement),
247 | but before the end tag for this element.
248 |
249 | """
250 | self._assert_is_element(subelement)
251 | self._children.append(subelement)
252 |
253 | def extend(self, elements):
254 | """Append subelements from a sequence.
255 |
256 | *elements* is a sequence with zero or more elements.
257 |
258 | """
259 | for element in elements:
260 | self._assert_is_element(element)
261 | self._children.extend(elements)
262 |
263 | def insert(self, index, subelement):
264 | """Insert *subelement* at position *index*."""
265 | self._assert_is_element(subelement)
266 | self._children.insert(index, subelement)
267 |
268 | def _assert_is_element(self, e):
269 | # Need to refer to the actual Python implementation, not the
270 | # shadowing C implementation.
271 | if not isinstance(e, _Element_Py):
272 | raise TypeError('expected an Element, not %s' % type(e).__name__)
273 |
274 | def remove(self, subelement):
275 | """Remove matching subelement.
276 |
277 | Unlike the find methods, this method compares elements based on
278 | identity, NOT ON tag value or contents. To remove subelements by
279 | other means, the easiest way is to use a list comprehension to
280 | select what elements to keep, and then use slice assignment to update
281 | the parent element.
282 |
283 | ValueError is raised if a matching element could not be found.
284 |
285 | """
286 | # assert iselement(element)
287 | self._children.remove(subelement)
288 |
289 | def getchildren(self):
290 | """(Deprecated) Return all subelements.
291 |
292 | Elements are returned in document order.
293 |
294 | """
295 | warnings.warn(
296 | "This method will be removed in future versions. "
297 | "Use 'list(elem)' or iteration over elem instead.",
298 | DeprecationWarning, stacklevel=2
299 | )
300 | return self._children
301 |
302 | def find(self, path, namespaces=None):
303 | """Find first matching element by tag name or path.
304 |
305 | *path* is a string having either an element tag or an XPath,
306 | *namespaces* is an optional mapping from namespace prefix to full name.
307 |
308 | Return the first matching element, or None if no element was found.
309 |
310 | """
311 | return ElementPath.find(self, path, namespaces)
312 |
313 | def findtext(self, path, default=None, namespaces=None):
314 | """Find text for first matching element by tag name or path.
315 |
316 | *path* is a string having either an element tag or an XPath,
317 | *default* is the value to return if the element was not found,
318 | *namespaces* is an optional mapping from namespace prefix to full name.
319 |
320 | Return text content of first matching element, or default value if
321 | none was found. Note that if an element is found having no text
322 | content, the empty string is returned.
323 |
324 | """
325 | return ElementPath.findtext(self, path, default, namespaces)
326 |
327 | def findall(self, path, namespaces=None):
328 | """Find all matching subelements by tag name or path.
329 |
330 | *path* is a string having either an element tag or an XPath,
331 | *namespaces* is an optional mapping from namespace prefix to full name.
332 |
333 | Returns list containing all matching elements in document order.
334 |
335 | """
336 | return ElementPath.findall(self, path, namespaces)
337 |
338 | def iterfind(self, path, namespaces=None):
339 | """Find all matching subelements by tag name or path.
340 |
341 | *path* is a string having either an element tag or an XPath,
342 | *namespaces* is an optional mapping from namespace prefix to full name.
343 |
344 | Return an iterable yielding all matching elements in document order.
345 |
346 | """
347 | return ElementPath.iterfind(self, path, namespaces)
348 |
349 | def clear(self):
350 | """Reset element.
351 |
352 | This function removes all subelements, clears all attributes, and sets
353 | the text and tail attributes to None.
354 |
355 | """
356 | self.attrib.clear()
357 | self._children = []
358 | self.text = self.tail = None
359 |
360 | def get(self, key, default=None):
361 | """Get element attribute.
362 |
363 | Equivalent to attrib.get, but some implementations may handle this a
364 | bit more efficiently. *key* is what attribute to look for, and
365 | *default* is what to return if the attribute was not found.
366 |
367 | Returns a string containing the attribute value, or the default if
368 | attribute was not found.
369 |
370 | """
371 | return self.attrib.get(key, default)
372 |
373 | def set(self, key, value):
374 | """Set element attribute.
375 |
376 | Equivalent to attrib[key] = value, but some implementations may handle
377 | this a bit more efficiently. *key* is what attribute to set, and
378 | *value* is the attribute value to set it to.
379 |
380 | """
381 | self.attrib[key] = value
382 |
383 | def keys(self):
384 | """Get list of attribute names.
385 |
386 | Names are returned in an arbitrary order, just like an ordinary
387 | Python dict. Equivalent to attrib.keys()
388 |
389 | """
390 | return self.attrib.keys()
391 |
392 | def items(self):
393 | """Get element attributes as a sequence.
394 |
395 | The attributes are returned in arbitrary order. Equivalent to
396 | attrib.items().
397 |
398 | Return a list of (name, value) tuples.
399 |
400 | """
401 | return self.attrib.items()
402 |
403 | def iter(self, tag=None):
404 | """Create tree iterator.
405 |
406 | The iterator loops over the element and all subelements in document
407 | order, returning all elements with a matching tag.
408 |
409 | If the tree structure is modified during iteration, new or removed
410 | elements may or may not be included. To get a stable set, use the
411 | list() function on the iterator, and loop over the resulting list.
412 |
413 | *tag* is what tags to look for (default is to return all elements)
414 |
415 | Return an iterator containing all the matching elements.
416 |
417 | """
418 | if tag == "*":
419 | tag = None
420 | if tag is None or self.tag == tag:
421 | yield self
422 | for e in self._children:
423 | yield from e.iter(tag)
424 |
425 | # compatibility
426 | def getiterator(self, tag=None):
427 | # Change for a DeprecationWarning in 1.4
428 | warnings.warn(
429 | "This method will be removed in future versions. "
430 | "Use 'elem.iter()' or 'list(elem.iter())' instead.",
431 | PendingDeprecationWarning, stacklevel=2
432 | )
433 | return list(self.iter(tag))
434 |
435 | def itertext(self):
436 | """Create text iterator.
437 |
438 | The iterator loops over the element and all subelements in document
439 | order, returning all inner text.
440 |
441 | """
442 | tag = self.tag
443 | if not isinstance(tag, str) and tag is not None:
444 | return
445 | t = self.text
446 | if t:
447 | yield t
448 | for e in self:
449 | yield from e.itertext()
450 | t = e.tail
451 | if t:
452 | yield t
453 |
454 |
455 | def SubElement(parent, tag, attrib={}, **extra):
456 | """Subelement factory which creates an element instance, and appends it
457 | to an existing parent.
458 |
459 | The element tag, attribute names, and attribute values can be either
460 | bytes or Unicode strings.
461 |
462 | *parent* is the parent element, *tag* is the subelements name, *attrib* is
463 | an optional directory containing element attributes, *extra* are
464 | additional attributes given as keyword arguments.
465 |
466 | """
467 | attrib = attrib.copy()
468 | attrib.update(extra)
469 | element = parent.makeelement(tag, attrib)
470 | parent.append(element)
471 | return element
472 |
473 |
474 | def Comment(text=None):
475 | """Comment element factory.
476 |
477 | This function creates a special element which the standard serializer
478 | serializes as an XML comment.
479 |
480 | *text* is a string containing the comment string.
481 |
482 | """
483 | element = Element(Comment)
484 | element.text = text
485 | return element
486 |
487 |
488 | def ProcessingInstruction(target, text=None):
489 | """Processing Instruction element factory.
490 |
491 | This function creates a special element which the standard serializer
492 | serializes as an XML comment.
493 |
494 | *target* is a string containing the processing instruction, *text* is a
495 | string containing the processing instruction contents, if any.
496 |
497 | """
498 | element = Element(ProcessingInstruction)
499 | element.text = target
500 | if text:
501 | element.text = element.text + " " + text
502 | return element
503 |
504 | PI = ProcessingInstruction
505 |
506 |
507 | class QName:
508 | """Qualified name wrapper.
509 |
510 | This class can be used to wrap a QName attribute value in order to get
511 | proper namespace handing on output.
512 |
513 | *text_or_uri* is a string containing the QName value either in the form
514 | {uri}local, or if the tag argument is given, the URI part of a QName.
515 |
516 | *tag* is an optional argument which if given, will make the first
517 | argument (text_or_uri) be interpreted as a URI, and this argument (tag)
518 | be interpreted as a local name.
519 |
520 | """
521 | def __init__(self, text_or_uri, tag=None):
522 | if tag:
523 | text_or_uri = "{%s}%s" % (text_or_uri, tag)
524 | self.text = text_or_uri
525 | def __str__(self):
526 | return self.text
527 | def __repr__(self):
528 | return '<%s %r>' % (self.__class__.__name__, self.text)
529 | def __hash__(self):
530 | return hash(self.text)
531 | def __le__(self, other):
532 | if isinstance(other, QName):
533 | return self.text <= other.text
534 | return self.text <= other
535 | def __lt__(self, other):
536 | if isinstance(other, QName):
537 | return self.text < other.text
538 | return self.text < other
539 | def __ge__(self, other):
540 | if isinstance(other, QName):
541 | return self.text >= other.text
542 | return self.text >= other
543 | def __gt__(self, other):
544 | if isinstance(other, QName):
545 | return self.text > other.text
546 | return self.text > other
547 | def __eq__(self, other):
548 | if isinstance(other, QName):
549 | return self.text == other.text
550 | return self.text == other
551 |
552 | # --------------------------------------------------------------------
553 |
554 |
555 | class ElementTree:
556 | """An XML element hierarchy.
557 |
558 | This class also provides support for serialization to and from
559 | standard XML.
560 |
561 | *element* is an optional root element node,
562 | *file* is an optional file handle or file name of an XML file whose
563 | contents will be used to initialize the tree with.
564 |
565 | """
566 | def __init__(self, element=None, file=None):
567 | # assert element is None or iselement(element)
568 | self._root = element # first node
569 | if file:
570 | self.parse(file)
571 |
572 | def getroot(self):
573 | """Return root element of this tree."""
574 | return self._root
575 |
576 | def _setroot(self, element):
577 | """Replace root element of this tree.
578 |
579 | This will discard the current contents of the tree and replace it
580 | with the given element. Use with care!
581 |
582 | """
583 | # assert iselement(element)
584 | self._root = element
585 |
586 | def parse(self, source, parser=None):
587 | """Load external XML document into element tree.
588 |
589 | *source* is a file name or file object, *parser* is an optional parser
590 | instance that defaults to XMLParser.
591 |
592 | ParseError is raised if the parser fails to parse the document.
593 |
594 | Returns the root element of the given source document.
595 |
596 | """
597 | close_source = False
598 | if not hasattr(source, "read"):
599 | source = open(source, "rb")
600 | close_source = True
601 | try:
602 | if parser is None:
603 | # If no parser was specified, create a default XMLParser
604 | parser = XMLParser()
605 | if hasattr(parser, '_parse_whole'):
606 | # The default XMLParser, when it comes from an accelerator,
607 | # can define an internal _parse_whole API for efficiency.
608 | # It can be used to parse the whole source without feeding
609 | # it with chunks.
610 | self._root = parser._parse_whole(source)
611 | return self._root
612 | while True:
613 | data = source.read(65536)
614 | if not data:
615 | break
616 | parser.feed(data)
617 | self._root = parser.close()
618 | return self._root
619 | finally:
620 | if close_source:
621 | source.close()
622 |
623 | def iter(self, tag=None):
624 | """Create and return tree iterator for the root element.
625 |
626 | The iterator loops over all elements in this tree, in document order.
627 |
628 | *tag* is a string with the tag name to iterate over
629 | (default is to return all elements).
630 |
631 | """
632 | # assert self._root is not None
633 | return self._root.iter(tag)
634 |
635 | # compatibility
636 | def getiterator(self, tag=None):
637 | # Change for a DeprecationWarning in 1.4
638 | warnings.warn(
639 | "This method will be removed in future versions. "
640 | "Use 'tree.iter()' or 'list(tree.iter())' instead.",
641 | PendingDeprecationWarning, stacklevel=2
642 | )
643 | return list(self.iter(tag))
644 |
645 | def find(self, path, namespaces=None):
646 | """Find first matching element by tag name or path.
647 |
648 | Same as getroot().find(path), which is Element.find()
649 |
650 | *path* is a string having either an element tag or an XPath,
651 | *namespaces* is an optional mapping from namespace prefix to full name.
652 |
653 | Return the first matching element, or None if no element was found.
654 |
655 | """
656 | # assert self._root is not None
657 | if path[:1] == "/":
658 | path = "." + path
659 | warnings.warn(
660 | "This search is broken in 1.3 and earlier, and will be "
661 | "fixed in a future version. If you rely on the current "
662 | "behaviour, change it to %r" % path,
663 | FutureWarning, stacklevel=2
664 | )
665 | return self._root.find(path, namespaces)
666 |
667 | def findtext(self, path, default=None, namespaces=None):
668 | """Find first matching element by tag name or path.
669 |
670 | Same as getroot().findtext(path), which is Element.findtext()
671 |
672 | *path* is a string having either an element tag or an XPath,
673 | *namespaces* is an optional mapping from namespace prefix to full name.
674 |
675 | Return the first matching element, or None if no element was found.
676 |
677 | """
678 | # assert self._root is not None
679 | if path[:1] == "/":
680 | path = "." + path
681 | warnings.warn(
682 | "This search is broken in 1.3 and earlier, and will be "
683 | "fixed in a future version. If you rely on the current "
684 | "behaviour, change it to %r" % path,
685 | FutureWarning, stacklevel=2
686 | )
687 | return self._root.findtext(path, default, namespaces)
688 |
689 | def findall(self, path, namespaces=None):
690 | """Find all matching subelements by tag name or path.
691 |
692 | Same as getroot().findall(path), which is Element.findall().
693 |
694 | *path* is a string having either an element tag or an XPath,
695 | *namespaces* is an optional mapping from namespace prefix to full name.
696 |
697 | Return list containing all matching elements in document order.
698 |
699 | """
700 | # assert self._root is not None
701 | if path[:1] == "/":
702 | path = "." + path
703 | warnings.warn(
704 | "This search is broken in 1.3 and earlier, and will be "
705 | "fixed in a future version. If you rely on the current "
706 | "behaviour, change it to %r" % path,
707 | FutureWarning, stacklevel=2
708 | )
709 | return self._root.findall(path, namespaces)
710 |
711 | def iterfind(self, path, namespaces=None):
712 | """Find all matching subelements by tag name or path.
713 |
714 | Same as getroot().iterfind(path), which is element.iterfind()
715 |
716 | *path* is a string having either an element tag or an XPath,
717 | *namespaces* is an optional mapping from namespace prefix to full name.
718 |
719 | Return an iterable yielding all matching elements in document order.
720 |
721 | """
722 | # assert self._root is not None
723 | if path[:1] == "/":
724 | path = "." + path
725 | warnings.warn(
726 | "This search is broken in 1.3 and earlier, and will be "
727 | "fixed in a future version. If you rely on the current "
728 | "behaviour, change it to %r" % path,
729 | FutureWarning, stacklevel=2
730 | )
731 | return self._root.iterfind(path, namespaces)
732 |
733 | def write(self, file_or_filename,
734 | encoding=None,
735 | xml_declaration=None,
736 | default_namespace=None,
737 | method=None, *,
738 | short_empty_elements=True):
739 | """Write element tree to a file as XML.
740 |
741 | Arguments:
742 | *file_or_filename* -- file name or a file object opened for writing
743 |
744 | *encoding* -- the output encoding (default: US-ASCII)
745 |
746 | *xml_declaration* -- bool indicating if an XML declaration should be
747 | added to the output. If None, an XML declaration
748 | is added if encoding IS NOT either of:
749 | US-ASCII, UTF-8, or Unicode
750 |
751 | *default_namespace* -- sets the default XML namespace (for "xmlns")
752 |
753 | *method* -- either "xml" (default), "html, "text", or "c14n"
754 |
755 | *short_empty_elements* -- controls the formatting of elements
756 | that contain no content. If True (default)
757 | they are emitted as a single self-closed
758 | tag, otherwise they are emitted as a pair
759 | of start/end tags
760 |
761 | """
762 | if not method:
763 | method = "xml"
764 | elif method not in _serialize:
765 | raise ValueError("unknown method %r" % method)
766 | if not encoding:
767 | if method == "c14n":
768 | encoding = "utf-8"
769 | else:
770 | encoding = "us-ascii"
771 | enc_lower = encoding.lower()
772 | with _get_writer(file_or_filename, enc_lower) as write:
773 | if method == "xml" and (xml_declaration or
774 | (xml_declaration is None and
775 | enc_lower not in ("utf-8", "us-ascii", "unicode"))):
776 | declared_encoding = encoding
777 | if enc_lower == "unicode":
778 | # Retrieve the default encoding for the xml declaration
779 | import locale
780 | declared_encoding = locale.getpreferredencoding()
781 | write("\n" % (
782 | declared_encoding,))
783 | if method == "text":
784 | _serialize_text(write, self._root)
785 | else:
786 | qnames, namespaces = _namespaces(self._root, default_namespace)
787 | serialize = _serialize[method]
788 | serialize(write, self._root, qnames, namespaces,
789 | short_empty_elements=short_empty_elements)
790 |
791 | def write_c14n(self, file):
792 | # lxml.etree compatibility. use output method instead
793 | return self.write(file, method="c14n")
794 |
795 | # --------------------------------------------------------------------
796 | # serialization support
797 |
798 | @contextlib.contextmanager
799 | def _get_writer(file_or_filename, encoding):
800 | # returns text write method and release all resources after using
801 | try:
802 | write = file_or_filename.write
803 | except AttributeError:
804 | # file_or_filename is a file name
805 | if encoding == "unicode":
806 | file = open(file_or_filename, "w")
807 | else:
808 | file = open(file_or_filename, "w", encoding=encoding,
809 | errors="xmlcharrefreplace")
810 | with file:
811 | yield file.write
812 | else:
813 | # file_or_filename is a file-like object
814 | # encoding determines if it is a text or binary writer
815 | if encoding == "unicode":
816 | # use a text writer as is
817 | yield write
818 | else:
819 | # wrap a binary writer with TextIOWrapper
820 | with contextlib.ExitStack() as stack:
821 | if isinstance(file_or_filename, io.BufferedIOBase):
822 | file = file_or_filename
823 | elif isinstance(file_or_filename, io.RawIOBase):
824 | file = io.BufferedWriter(file_or_filename)
825 | # Keep the original file open when the BufferedWriter is
826 | # destroyed
827 | stack.callback(file.detach)
828 | else:
829 | # This is to handle passed objects that aren't in the
830 | # IOBase hierarchy, but just have a write method
831 | file = io.BufferedIOBase()
832 | file.writable = lambda: True
833 | file.write = write
834 | try:
835 | # TextIOWrapper uses this methods to determine
836 | # if BOM (for UTF-16, etc) should be added
837 | file.seekable = file_or_filename.seekable
838 | file.tell = file_or_filename.tell
839 | except AttributeError:
840 | pass
841 | file = io.TextIOWrapper(file,
842 | encoding=encoding,
843 | errors="xmlcharrefreplace",
844 | newline="\n")
845 | # Keep the original file open when the TextIOWrapper is
846 | # destroyed
847 | stack.callback(file.detach)
848 | yield file.write
849 |
850 | def _namespaces(elem, default_namespace=None):
851 | # identify namespaces used in this tree
852 |
853 | # maps qnames to *encoded* prefix:local names
854 | qnames = {None: None}
855 |
856 | # maps uri:s to prefixes
857 | namespaces = {}
858 | if default_namespace:
859 | namespaces[default_namespace] = ""
860 |
861 | def add_qname(qname):
862 | # calculate serialized qname representation
863 | try:
864 | if qname[:1] == "{":
865 | uri, tag = qname[1:].rsplit("}", 1)
866 | prefix = namespaces.get(uri)
867 | if prefix is None:
868 | prefix = _namespace_map.get(uri)
869 | if prefix is None:
870 | prefix = "ns%d" % len(namespaces)
871 | if prefix != "xml":
872 | namespaces[uri] = prefix
873 | if prefix:
874 | qnames[qname] = "%s:%s" % (prefix, tag)
875 | else:
876 | qnames[qname] = tag # default element
877 | else:
878 | if default_namespace:
879 | # FIXME: can this be handled in XML 1.0?
880 | raise ValueError(
881 | "cannot use non-qualified names with "
882 | "default_namespace option"
883 | )
884 | qnames[qname] = qname
885 | except TypeError:
886 | _raise_serialization_error(qname)
887 |
888 | # populate qname and namespaces table
889 | for elem in elem.iter():
890 | tag = elem.tag
891 | if isinstance(tag, QName):
892 | if tag.text not in qnames:
893 | add_qname(tag.text)
894 | elif isinstance(tag, str):
895 | if tag not in qnames:
896 | add_qname(tag)
897 | elif tag is not None and tag is not Comment and tag is not PI:
898 | _raise_serialization_error(tag)
899 | for key, value in elem.items():
900 | if isinstance(key, QName):
901 | key = key.text
902 | if key not in qnames:
903 | add_qname(key)
904 | if isinstance(value, QName) and value.text not in qnames:
905 | add_qname(value.text)
906 | text = elem.text
907 | if isinstance(text, QName) and text.text not in qnames:
908 | add_qname(text.text)
909 | return qnames, namespaces
910 |
911 | def _serialize_xml(write, elem, qnames, namespaces,
912 | short_empty_elements, **kwargs):
913 | tag = elem.tag
914 | text = elem.text
915 | if tag is Comment:
916 | write("" % text)
917 | elif tag is ProcessingInstruction:
918 | write("%s?>" % text)
919 | else:
920 | tag = qnames[tag]
921 | if tag is None:
922 | if text:
923 | write(_escape_cdata(text))
924 | for e in elem:
925 | _serialize_xml(write, e, qnames, None,
926 | short_empty_elements=short_empty_elements)
927 | else:
928 | write("<" + tag)
929 | items = list(elem.items())
930 | if items or namespaces:
931 | if namespaces:
932 | for v, k in sorted(namespaces.items(),
933 | key=lambda x: x[1]): # sort on prefix
934 | if k:
935 | k = ":" + k
936 | write(" xmlns%s=\"%s\"" % (
937 | k,
938 | _escape_attrib(v)
939 | ))
940 | for k, v in sorted(items): # lexical order
941 | if isinstance(k, QName):
942 | k = k.text
943 | if isinstance(v, QName):
944 | v = qnames[v.text]
945 | else:
946 | v = _escape_attrib(v)
947 | write(" %s=\"%s\"" % (qnames[k], v))
948 | if text or len(elem) or not short_empty_elements:
949 | write(">")
950 | if text:
951 | write(_escape_cdata(text))
952 | for e in elem:
953 | _serialize_xml(write, e, qnames, None,
954 | short_empty_elements=short_empty_elements)
955 | write("" + tag + ">")
956 | else:
957 | write(" />")
958 | if elem.tail:
959 | write(_escape_cdata(elem.tail))
960 |
961 | HTML_EMPTY = ("area", "base", "basefont", "br", "col", "frame", "hr",
962 | "img", "input", "isindex", "link", "meta", "param")
963 |
964 | try:
965 | HTML_EMPTY = set(HTML_EMPTY)
966 | except NameError:
967 | pass
968 |
969 | def _serialize_html(write, elem, qnames, namespaces, **kwargs):
970 | tag = elem.tag
971 | text = elem.text
972 | if tag is Comment:
973 | write("" % _escape_cdata(text))
974 | elif tag is ProcessingInstruction:
975 | write("%s?>" % _escape_cdata(text))
976 | else:
977 | tag = qnames[tag]
978 | if tag is None:
979 | if text:
980 | write(_escape_cdata(text))
981 | for e in elem:
982 | _serialize_html(write, e, qnames, None)
983 | else:
984 | write("<" + tag)
985 | items = list(elem.items())
986 | if items or namespaces:
987 | if namespaces:
988 | for v, k in sorted(namespaces.items(),
989 | key=lambda x: x[1]): # sort on prefix
990 | if k:
991 | k = ":" + k
992 | write(" xmlns%s=\"%s\"" % (
993 | k,
994 | _escape_attrib(v)
995 | ))
996 | for k, v in sorted(items): # lexical order
997 | if isinstance(k, QName):
998 | k = k.text
999 | if isinstance(v, QName):
1000 | v = qnames[v.text]
1001 | else:
1002 | v = _escape_attrib_html(v)
1003 | # FIXME: handle boolean attributes
1004 | write(" %s=\"%s\"" % (qnames[k], v))
1005 | write(">")
1006 | ltag = tag.lower()
1007 | if text:
1008 | if ltag == "script" or ltag == "style":
1009 | write(text)
1010 | else:
1011 | write(_escape_cdata(text))
1012 | for e in elem:
1013 | _serialize_html(write, e, qnames, None)
1014 | if ltag not in HTML_EMPTY:
1015 | write("" + tag + ">")
1016 | if elem.tail:
1017 | write(_escape_cdata(elem.tail))
1018 |
1019 | def _serialize_text(write, elem):
1020 | for part in elem.itertext():
1021 | write(part)
1022 | if elem.tail:
1023 | write(elem.tail)
1024 |
1025 | _serialize = {
1026 | "xml": _serialize_xml,
1027 | "html": _serialize_html,
1028 | "text": _serialize_text,
1029 | # this optional method is imported at the end of the module
1030 | # "c14n": _serialize_c14n,
1031 | }
1032 |
1033 |
1034 | def register_namespace(prefix, uri):
1035 | """Register a namespace prefix.
1036 |
1037 | The registry is global, and any existing mapping for either the
1038 | given prefix or the namespace URI will be removed.
1039 |
1040 | *prefix* is the namespace prefix, *uri* is a namespace uri. Tags and
1041 | attributes in this namespace will be serialized with prefix if possible.
1042 |
1043 | ValueError is raised if prefix is reserved or is invalid.
1044 |
1045 | """
1046 | if re.match(r"ns\d+$", prefix):
1047 | raise ValueError("Prefix format reserved for internal use")
1048 | for k, v in list(_namespace_map.items()):
1049 | if k == uri or v == prefix:
1050 | del _namespace_map[k]
1051 | _namespace_map[uri] = prefix
1052 |
1053 | _namespace_map = {
1054 | # "well-known" namespace prefixes
1055 | "http://www.w3.org/XML/1998/namespace": "xml",
1056 | "http://www.w3.org/1999/xhtml": "html",
1057 | "http://www.w3.org/1999/02/22-rdf-syntax-ns#": "rdf",
1058 | "http://schemas.xmlsoap.org/wsdl/": "wsdl",
1059 | # xml schema
1060 | "http://www.w3.org/2001/XMLSchema": "xs",
1061 | "http://www.w3.org/2001/XMLSchema-instance": "xsi",
1062 | # dublin core
1063 | "http://purl.org/dc/elements/1.1/": "dc",
1064 | }
1065 | # For tests and troubleshooting
1066 | register_namespace._namespace_map = _namespace_map
1067 |
1068 | def _raise_serialization_error(text):
1069 | raise TypeError(
1070 | "cannot serialize %r (type %s)" % (text, type(text).__name__)
1071 | )
1072 |
1073 | def _escape_cdata(text):
1074 | # escape character data
1075 | try:
1076 | # it's worth avoiding do-nothing calls for strings that are
1077 | # shorter than 500 character, or so. assume that's, by far,
1078 | # the most common case in most applications.
1079 | if "&" in text:
1080 | text = text.replace("&", "&")
1081 | if "<" in text:
1082 | text = text.replace("<", "<")
1083 | if ">" in text:
1084 | text = text.replace(">", ">")
1085 | return text
1086 | except (TypeError, AttributeError):
1087 | _raise_serialization_error(text)
1088 |
1089 | def _escape_attrib(text):
1090 | # escape attribute value
1091 | try:
1092 | if "&" in text:
1093 | text = text.replace("&", "&")
1094 | if "<" in text:
1095 | text = text.replace("<", "<")
1096 | if ">" in text:
1097 | text = text.replace(">", ">")
1098 | if "\"" in text:
1099 | text = text.replace("\"", """)
1100 | # The following business with carriage returns is to satisfy
1101 | # Section 2.11 of the XML specification, stating that
1102 | # CR or CR LN should be replaced with just LN
1103 | # http://www.w3.org/TR/REC-xml/#sec-line-ends
1104 | if "\r\n" in text:
1105 | text = text.replace("\r\n", "\n")
1106 | if "\r" in text:
1107 | text = text.replace("\r", "\n")
1108 | #The following four lines are issue 17582
1109 | if "\n" in text:
1110 | text = text.replace("\n", "
")
1111 | if "\t" in text:
1112 | text = text.replace("\t", " ")
1113 | return text
1114 | except (TypeError, AttributeError):
1115 | _raise_serialization_error(text)
1116 |
1117 | def _escape_attrib_html(text):
1118 | # escape attribute value
1119 | try:
1120 | if "&" in text:
1121 | text = text.replace("&", "&")
1122 | if ">" in text:
1123 | text = text.replace(">", ">")
1124 | if "\"" in text:
1125 | text = text.replace("\"", """)
1126 | return text
1127 | except (TypeError, AttributeError):
1128 | _raise_serialization_error(text)
1129 |
1130 | # --------------------------------------------------------------------
1131 |
1132 | def tostring(element, encoding=None, method=None, *,
1133 | short_empty_elements=True):
1134 | """Generate string representation of XML element.
1135 |
1136 | All subelements are included. If encoding is "unicode", a string
1137 | is returned. Otherwise a bytestring is returned.
1138 |
1139 | *element* is an Element instance, *encoding* is an optional output
1140 | encoding defaulting to US-ASCII, *method* is an optional output which can
1141 | be one of "xml" (default), "html", "text" or "c14n".
1142 |
1143 | Returns an (optionally) encoded string containing the XML data.
1144 |
1145 | """
1146 | stream = io.StringIO() if encoding == 'unicode' else io.BytesIO()
1147 | ElementTree(element).write(stream, encoding, method=method,
1148 | short_empty_elements=short_empty_elements)
1149 | return stream.getvalue()
1150 |
1151 | class _ListDataStream(io.BufferedIOBase):
1152 | """An auxiliary stream accumulating into a list reference."""
1153 | def __init__(self, lst):
1154 | self.lst = lst
1155 |
1156 | def writable(self):
1157 | return True
1158 |
1159 | def seekable(self):
1160 | return True
1161 |
1162 | def write(self, b):
1163 | self.lst.append(b)
1164 |
1165 | def tell(self):
1166 | return len(self.lst)
1167 |
1168 | def tostringlist(element, encoding=None, method=None, *,
1169 | short_empty_elements=True):
1170 | lst = []
1171 | stream = _ListDataStream(lst)
1172 | ElementTree(element).write(stream, encoding, method=method,
1173 | short_empty_elements=short_empty_elements)
1174 | return lst
1175 |
1176 |
1177 | def dump(elem):
1178 | """Write element tree or element structure to sys.stdout.
1179 |
1180 | This function should be used for debugging only.
1181 |
1182 | *elem* is either an ElementTree, or a single Element. The exact output
1183 | format is implementation dependent. In this version, it's written as an
1184 | ordinary XML file.
1185 |
1186 | """
1187 | # debugging
1188 | if not isinstance(elem, ElementTree):
1189 | elem = ElementTree(elem)
1190 | elem.write(sys.stdout, encoding="unicode")
1191 | tail = elem.getroot().tail
1192 | if not tail or tail[-1] != "\n":
1193 | sys.stdout.write("\n")
1194 |
1195 | # --------------------------------------------------------------------
1196 | # parsing
1197 |
1198 |
1199 | def parse(source, parser=None):
1200 | """Parse XML document into element tree.
1201 |
1202 | *source* is a filename or file object containing XML data,
1203 | *parser* is an optional parser instance defaulting to XMLParser.
1204 |
1205 | Return an ElementTree instance.
1206 |
1207 | """
1208 | tree = ElementTree()
1209 | tree.parse(source, parser)
1210 | return tree
1211 |
1212 |
1213 | def iterparse(source, events=None, parser=None):
1214 | """Incrementally parse XML document into ElementTree.
1215 |
1216 | This class also reports what's going on to the user based on the
1217 | *events* it is initialized with. The supported events are the strings
1218 | "start", "end", "start-ns" and "end-ns" (the "ns" events are used to get
1219 | detailed namespace information). If *events* is omitted, only
1220 | "end" events are reported.
1221 |
1222 | *source* is a filename or file object containing XML data, *events* is
1223 | a list of events to report back, *parser* is an optional parser instance.
1224 |
1225 | Returns an iterator providing (event, elem) pairs.
1226 |
1227 | """
1228 | # Use the internal, undocumented _parser argument for now; When the
1229 | # parser argument of iterparse is removed, this can be killed.
1230 | pullparser = XMLPullParser(events=events, _parser=parser)
1231 | def iterator():
1232 | try:
1233 | while True:
1234 | yield from pullparser.read_events()
1235 | # load event buffer
1236 | data = source.read(16 * 1024)
1237 | if not data:
1238 | break
1239 | pullparser.feed(data)
1240 | root = pullparser._close_and_return_root()
1241 | yield from pullparser.read_events()
1242 | it.root = root
1243 | finally:
1244 | if close_source:
1245 | source.close()
1246 |
1247 | class IterParseIterator(collections.Iterator):
1248 | __next__ = iterator().__next__
1249 | it = IterParseIterator()
1250 | it.root = None
1251 | del iterator, IterParseIterator
1252 |
1253 | close_source = False
1254 | if not hasattr(source, "read"):
1255 | source = open(source, "rb")
1256 | close_source = True
1257 |
1258 | return it
1259 |
1260 |
1261 | class XMLPullParser:
1262 |
1263 | def __init__(self, events=None, *, _parser=None):
1264 | # The _parser argument is for internal use only and must not be relied
1265 | # upon in user code. It will be removed in a future release.
1266 | # See http://bugs.python.org/issue17741 for more details.
1267 |
1268 | self._events_queue = collections.deque()
1269 | self._parser = _parser or XMLParser(target=TreeBuilder())
1270 | # wire up the parser for event reporting
1271 | if events is None:
1272 | events = ("end",)
1273 | self._parser._setevents(self._events_queue, events)
1274 |
1275 | def feed(self, data):
1276 | """Feed encoded data to parser."""
1277 | if self._parser is None:
1278 | raise ValueError("feed() called after end of stream")
1279 | if data:
1280 | try:
1281 | self._parser.feed(data)
1282 | except SyntaxError as exc:
1283 | self._events_queue.append(exc)
1284 |
1285 | def _close_and_return_root(self):
1286 | # iterparse needs this to set its root attribute properly :(
1287 | root = self._parser.close()
1288 | self._parser = None
1289 | return root
1290 |
1291 | def close(self):
1292 | """Finish feeding data to parser.
1293 |
1294 | Unlike XMLParser, does not return the root element. Use
1295 | read_events() to consume elements from XMLPullParser.
1296 | """
1297 | self._close_and_return_root()
1298 |
1299 | def read_events(self):
1300 | """Return an iterator over currently available (event, elem) pairs.
1301 |
1302 | Events are consumed from the internal event queue as they are
1303 | retrieved from the iterator.
1304 | """
1305 | events = self._events_queue
1306 | while events:
1307 | event = events.popleft()
1308 | if isinstance(event, Exception):
1309 | raise event
1310 | else:
1311 | yield event
1312 |
1313 |
1314 | def XML(text, parser=None):
1315 | """Parse XML document from string constant.
1316 |
1317 | This function can be used to embed "XML Literals" in Python code.
1318 |
1319 | *text* is a string containing XML data, *parser* is an
1320 | optional parser instance, defaulting to the standard XMLParser.
1321 |
1322 | Returns an Element instance.
1323 |
1324 | """
1325 | if not parser:
1326 | parser = XMLParser(target=TreeBuilder())
1327 | parser.feed(text)
1328 | return parser.close()
1329 |
1330 |
1331 | def XMLID(text, parser=None):
1332 | """Parse XML document from string constant for its IDs.
1333 |
1334 | *text* is a string containing XML data, *parser* is an
1335 | optional parser instance, defaulting to the standard XMLParser.
1336 |
1337 | Returns an (Element, dict) tuple, in which the
1338 | dict maps element id:s to elements.
1339 |
1340 | """
1341 | if not parser:
1342 | parser = XMLParser(target=TreeBuilder())
1343 | parser.feed(text)
1344 | tree = parser.close()
1345 | ids = {}
1346 | for elem in tree.iter():
1347 | id = elem.get("id")
1348 | if id:
1349 | ids[id] = elem
1350 | return tree, ids
1351 |
1352 | # Parse XML document from string constant. Alias for XML().
1353 | fromstring = XML
1354 |
1355 | def fromstringlist(sequence, parser=None):
1356 | """Parse XML document from sequence of string fragments.
1357 |
1358 | *sequence* is a list of other sequence, *parser* is an optional parser
1359 | instance, defaulting to the standard XMLParser.
1360 |
1361 | Returns an Element instance.
1362 |
1363 | """
1364 | if not parser:
1365 | parser = XMLParser(target=TreeBuilder())
1366 | for text in sequence:
1367 | parser.feed(text)
1368 | return parser.close()
1369 |
1370 | # --------------------------------------------------------------------
1371 |
1372 |
1373 | class TreeBuilder:
1374 | """Generic element structure builder.
1375 |
1376 | This builder converts a sequence of start, data, and end method
1377 | calls to a well-formed element structure.
1378 |
1379 | You can use this class to build an element structure using a custom XML
1380 | parser, or a parser for some other XML-like format.
1381 |
1382 | *element_factory* is an optional element factory which is called
1383 | to create new Element instances, as necessary.
1384 |
1385 | """
1386 | def __init__(self, element_factory=None):
1387 | self._data = [] # data collector
1388 | self._elem = [] # element stack
1389 | self._last = None # last element
1390 | self._tail = None # true if we're after an end tag
1391 | if element_factory is None:
1392 | element_factory = Element
1393 | self._factory = element_factory
1394 |
1395 | def close(self):
1396 | """Flush builder buffers and return toplevel document Element."""
1397 | assert len(self._elem) == 0, "missing end tags"
1398 | assert self._last is not None, "missing toplevel element"
1399 | return self._last
1400 |
1401 | def _flush(self):
1402 | if self._data:
1403 | if self._last is not None:
1404 | text = "".join(self._data)
1405 | if self._tail:
1406 | assert self._last.tail is None, "internal error (tail)"
1407 | self._last.tail = text
1408 | else:
1409 | assert self._last.text is None, "internal error (text)"
1410 | self._last.text = text
1411 | self._data = []
1412 |
1413 | def data(self, data):
1414 | """Add text to current element."""
1415 | self._data.append(data)
1416 |
1417 | def start(self, tag, attrs):
1418 | """Open new element and return it.
1419 |
1420 | *tag* is the element name, *attrs* is a dict containing element
1421 | attributes.
1422 |
1423 | """
1424 | self._flush()
1425 | self._last = elem = self._factory(tag, attrs)
1426 | if self._elem:
1427 | self._elem[-1].append(elem)
1428 | self._elem.append(elem)
1429 | self._tail = 0
1430 | return elem
1431 |
1432 | def end(self, tag):
1433 | """Close and return current Element.
1434 |
1435 | *tag* is the element name.
1436 |
1437 | """
1438 | self._flush()
1439 | self._last = self._elem.pop()
1440 | assert self._last.tag == tag,\
1441 | "end tag mismatch (expected %s, got %s)" % (
1442 | self._last.tag, tag)
1443 | self._tail = 1
1444 | return self._last
1445 |
1446 |
1447 | # also see ElementTree and TreeBuilder
1448 | class XMLParser:
1449 | """Element structure builder for XML source data based on the expat parser.
1450 |
1451 | *html* are predefined HTML entities (deprecated and not supported),
1452 | *target* is an optional target object which defaults to an instance of the
1453 | standard TreeBuilder class, *encoding* is an optional encoding string
1454 | which if given, overrides the encoding specified in the XML file:
1455 | http://www.iana.org/assignments/character-sets
1456 |
1457 | """
1458 |
1459 | def __init__(self, html=0, target=None, encoding=None):
1460 | try:
1461 | from xml.parsers import expat
1462 | except ImportError:
1463 | try:
1464 | import pyexpat as expat
1465 | except ImportError:
1466 | raise ImportError(
1467 | "No module named expat; use SimpleXMLTreeBuilder instead"
1468 | )
1469 | parser = expat.ParserCreate(encoding, "}")
1470 | if target is None:
1471 | target = TreeBuilder()
1472 | # underscored names are provided for compatibility only
1473 | self.parser = self._parser = parser
1474 | self.target = self._target = target
1475 | self._error = expat.error
1476 | self._names = {} # name memo cache
1477 | # main callbacks
1478 | parser.DefaultHandlerExpand = self._default
1479 | if hasattr(target, 'start'):
1480 | parser.StartElementHandler = self._start
1481 | if hasattr(target, 'end'):
1482 | parser.EndElementHandler = self._end
1483 | if hasattr(target, 'data'):
1484 | parser.CharacterDataHandler = target.data
1485 | # miscellaneous callbacks
1486 | if hasattr(target, 'comment'):
1487 | parser.CommentHandler = target.comment
1488 | if hasattr(target, 'pi'):
1489 | parser.ProcessingInstructionHandler = target.pi
1490 | # Configure pyexpat: buffering, new-style attribute handling.
1491 | parser.buffer_text = 1
1492 | parser.ordered_attributes = 1
1493 | parser.specified_attributes = 1
1494 | self._doctype = None
1495 | self.entity = {}
1496 | try:
1497 | self.version = "Expat %d.%d.%d" % expat.version_info
1498 | except AttributeError:
1499 | pass # unknown
1500 |
1501 | def _setevents(self, events_queue, events_to_report):
1502 | # Internal API for XMLPullParser
1503 | # events_to_report: a list of events to report during parsing (same as
1504 | # the *events* of XMLPullParser's constructor.
1505 | # events_queue: a list of actual parsing events that will be populated
1506 | # by the underlying parser.
1507 | #
1508 | parser = self._parser
1509 | append = events_queue.append
1510 | for event_name in events_to_report:
1511 | if event_name == "start":
1512 | parser.ordered_attributes = 1
1513 | parser.specified_attributes = 1
1514 | def handler(tag, attrib_in, event=event_name, append=append,
1515 | start=self._start):
1516 | append((event, start(tag, attrib_in)))
1517 | parser.StartElementHandler = handler
1518 | elif event_name == "end":
1519 | def handler(tag, event=event_name, append=append,
1520 | end=self._end):
1521 | append((event, end(tag)))
1522 | parser.EndElementHandler = handler
1523 | elif event_name == "start-ns":
1524 | def handler(prefix, uri, event=event_name, append=append):
1525 | append((event, (prefix or "", uri or "")))
1526 | parser.StartNamespaceDeclHandler = handler
1527 | elif event_name == "end-ns":
1528 | def handler(prefix, event=event_name, append=append):
1529 | append((event, None))
1530 | parser.EndNamespaceDeclHandler = handler
1531 | else:
1532 | raise ValueError("unknown event %r" % event_name)
1533 |
1534 | def _raiseerror(self, value):
1535 | err = ParseError(value)
1536 | err.code = value.code
1537 | err.position = value.lineno, value.offset
1538 | raise err
1539 |
1540 | def _fixname(self, key):
1541 | # expand qname, and convert name string to ascii, if possible
1542 | try:
1543 | name = self._names[key]
1544 | except KeyError:
1545 | name = key
1546 | if "}" in name:
1547 | name = "{" + name
1548 | self._names[key] = name
1549 | return name
1550 |
1551 | def _start(self, tag, attr_list):
1552 | # Handler for expat's StartElementHandler. Since ordered_attributes
1553 | # is set, the attributes are reported as a list of alternating
1554 | # attribute name,value.
1555 | fixname = self._fixname
1556 | tag = fixname(tag)
1557 | attrib = {}
1558 | if attr_list:
1559 | for i in range(0, len(attr_list), 2):
1560 | attrib[fixname(attr_list[i])] = attr_list[i+1]
1561 | return self.target.start(tag, attrib)
1562 |
1563 | def _end(self, tag):
1564 | return self.target.end(self._fixname(tag))
1565 |
1566 | def _default(self, text):
1567 | prefix = text[:1]
1568 | if prefix == "&":
1569 | # deal with undefined entities
1570 | try:
1571 | data_handler = self.target.data
1572 | except AttributeError:
1573 | return
1574 | try:
1575 | data_handler(self.entity[text[1:-1]])
1576 | except KeyError:
1577 | from xml.parsers import expat
1578 | err = expat.error(
1579 | "undefined entity %s: line %d, column %d" %
1580 | (text, self.parser.ErrorLineNumber,
1581 | self.parser.ErrorColumnNumber)
1582 | )
1583 | err.code = 11 # XML_ERROR_UNDEFINED_ENTITY
1584 | err.lineno = self.parser.ErrorLineNumber
1585 | err.offset = self.parser.ErrorColumnNumber
1586 | raise err
1587 | elif prefix == "<" and text[:9] == "":
1592 | self._doctype = None
1593 | return
1594 | text = text.strip()
1595 | if not text:
1596 | return
1597 | self._doctype.append(text)
1598 | n = len(self._doctype)
1599 | if n > 2:
1600 | type = self._doctype[1]
1601 | if type == "PUBLIC" and n == 4:
1602 | name, type, pubid, system = self._doctype
1603 | if pubid:
1604 | pubid = pubid[1:-1]
1605 | elif type == "SYSTEM" and n == 3:
1606 | name, type, system = self._doctype
1607 | pubid = None
1608 | else:
1609 | return
1610 | if hasattr(self.target, "doctype"):
1611 | self.target.doctype(name, pubid, system[1:-1])
1612 | elif self.doctype != self._XMLParser__doctype:
1613 | # warn about deprecated call
1614 | self._XMLParser__doctype(name, pubid, system[1:-1])
1615 | self.doctype(name, pubid, system[1:-1])
1616 | self._doctype = None
1617 |
1618 | def doctype(self, name, pubid, system):
1619 | """(Deprecated) Handle doctype declaration
1620 |
1621 | *name* is the Doctype name, *pubid* is the public identifier,
1622 | and *system* is the system identifier.
1623 |
1624 | """
1625 | warnings.warn(
1626 | "This method of XMLParser is deprecated. Define doctype() "
1627 | "method on the TreeBuilder target.",
1628 | DeprecationWarning,
1629 | )
1630 |
1631 | # sentinel, if doctype is redefined in a subclass
1632 | __doctype = doctype
1633 |
1634 | def feed(self, data):
1635 | """Feed encoded data to parser."""
1636 | try:
1637 | self.parser.Parse(data, 0)
1638 | except self._error as v:
1639 | self._raiseerror(v)
1640 |
1641 | def close(self):
1642 | """Finish feeding data to parser and return element structure."""
1643 | try:
1644 | self.parser.Parse("", 1) # end of data
1645 | except self._error as v:
1646 | self._raiseerror(v)
1647 | try:
1648 | close_handler = self.target.close
1649 | except AttributeError:
1650 | pass
1651 | else:
1652 | return close_handler()
1653 | finally:
1654 | # get rid of circular references
1655 | del self.parser, self._parser
1656 | del self.target, self._target
1657 |
1658 |
1659 | # Import the C accelerators
1660 | try:
1661 | # Element is going to be shadowed by the C implementation. We need to keep
1662 | # the Python version of it accessible for some "creative" by external code
1663 | # (see tests)
1664 | _Element_Py = Element
1665 |
1666 | # Element, SubElement, ParseError, TreeBuilder, XMLParser
1667 | # This took FOREVER TO FIND
1668 | # from _elementtree import *
1669 | except ImportError:
1670 | pass
1671 |
--------------------------------------------------------------------------------