Also remove use of .credentials file
This commit is contained in:
Hal Wine 2021-05-01 14:08:35 -07:00 коммит произвёл GitHub
Родитель 94af68c3a7
Коммит 62a28ac8da
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
32 изменённых файлов: 2572 добавлений и 508 удалений

10
.gitignore поставляемый
Просмотреть файл

@ -5,3 +5,13 @@ cache/*\.json
.venv
# for jupyter notebooks
.ipynb*
.jupyter
# for databases created by get_active_hooks
*\.db
# directories created when mounted as $HOME from docker image
.bash_history
.ipython/
.local/
.vscode/

Просмотреть файл

@ -0,0 +1,8 @@
# disable browser launch (it's in a container)
c.NotebookApp.open_browser = False
# Set connection string
# c.NotebookApp.portInt = 10002
c.NotebookApp.custom_display_url = "http://localhost:10002"
# disable security locally
c.NotebookApp.token = ""
c.NotebookApp.password = ""

Просмотреть файл

@ -19,18 +19,17 @@ repos:
hooks:
- id: black
## pyupgrade only supports version >py2
## - repo: https://github.com/asottile/pyupgrade
## rev: v2.7.2
## hooks:
## - id: pyupgrade
## args: ["--py36-plus"]
- repo: https://github.com/asottile/pyupgrade
rev: v2.14.0
hooks:
- id: pyupgrade
args: ["--py37-plus"]
- repo: https://github.com/jumanjihouse/pre-commit-hooks
rev: 2.1.5
rev: 2.1.5 # or specific git tag
hooks:
- id: forbid-binary
- id: markdownlint # Configure in .mdlrc
## - id: markdownlint # Configure in .mdlrc
- id: shellcheck
- repo: https://github.com/IamTheFij/docker-pre-commit

Просмотреть файл

@ -66,6 +66,10 @@
{
"path": "detect_secrets.filters.allowlist.is_line_allowlisted"
},
{
"path": "detect_secrets.filters.common.is_baseline_file",
"filename": ".secrets.baseline"
},
{
"path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
"min_level": 2
@ -98,6 +102,16 @@
"path": "detect_secrets.filters.heuristic.is_templated_secret"
}
],
"results": {},
"generated_at": "2021-05-01T10:54:26Z"
"results": {
"tests/test_get_org_info.py": [
{
"type": "Base64 High Entropy String",
"filename": "tests/test_get_org_info.py",
"hashed_secret": "4dca8daf537c805269b34a7750ff83dab2eb6450",
"is_verified": false,
"line_number": 9
}
]
},
"generated_at": "2021-05-01T11:06:23Z"
}

Просмотреть файл

@ -1,8 +1,8 @@
# Community Participation Guidelines
This repository is governed by Mozilla's code of conduct and etiquette guidelines.
This repository is governed by Mozilla's code of conduct and etiquette guidelines.
For more details, please read the
[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
## How to Report
For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page.

Просмотреть файл

@ -35,7 +35,7 @@ Mozilla Public License Version 2.0
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"

Просмотреть файл

@ -1,14 +1,77 @@
VENV_NAME=venv
VENV_NAME:=venv
github3_version:=1.1.2
port := 10002
DOCKER_OPTS :=
.PHONY: help
help:
@echo "Targets available"
@echo ""
@echo " help this message"
@echo " dev create & run a docker image based on working directory"
@echo " run-dev run a docker image previously created"
@echo " run-update run with modifiable current directory"
@echo " $(VENV_NAME) create a local virtualenv for old style development"
$(VENV_NAME):
virtualenv --python=python2.7 $@
virtualenv --python=python3.7 $@
. $(VENV_NAME)/bin/activate && echo req*.txt | xargs -n1 pip install -r
@echo "Virtualenv created in $(VENV_NAME). You must activate before continuing."
false
1.0a4:
repo2docker --image-name 'github-org-script:1.0a4' \
--ref repo2docker \
https://github.com/hwine/github-org-scripts \
SHELL := /bin/bash
.PHONY: dev
dev: jupyter-config
-docker rmi dev:$(github3_version)
repo2docker --image-name "dev:$(github3_version)" --no-run .
.PHONY: run-dev
run-dev:
$(SHELL) -c ' ( export GITHUB_PAT=$$(pass show Mozilla/moz-hwine-PAT) ; \
[[ -z $$GITHUB_PAT ]] && exit 3 ; \
docker run --rm --publish-all \
--env "GITHUB_PAT" \
--publish $(port):8888 \
dev:$(github3_version) \
& \
job_pid=$$! ; \
sleep 5 ; \
docker ps --filter "ancestor=dev:$(github3_version)" ; \
wait $$job_pid ; \
) '
.PHONY: run-update
run-update: jupyter-config
$(SHELL) -c ' ( export GITHUB_PAT=$$(pass show Mozilla/moz-hwine-PAT) ; \
[[ -z $$GITHUB_PAT ]] && exit 3 ; \
docker run --rm --publish-all \
$(DOCKER_OPTS) \
--env "GITHUB_PAT" \
--publish $(port):8888 \
--volume "$$PWD:/home/$$USER" \
dev:$(github3_version) \
& \
job_pid=$$! ; \
sleep 5 ; \
docker ps --filter "ancestor=dev:$(github3_version)" ; \
wait $$job_pid ; \
) '
.PHONY: debug-update
debug-update:
$(MAKE) DOCKER_OPTS="--security-opt=seccomp:unconfined" run-update
jupyter-config: .jupyter/jupyter_notebook_config.py
.jupyter/jupyter_notebook_config.py:
echo -e >$@ \
"# disable browser launch (it's in a container)\n"\
"c.NotebookApp.open_browser = False\n"\
"# Set connection string\n"\
#"c.NotebookApp.portInt = $(port)\n"\
"c.NotebookApp.custom_display_url = 'http://localhost:$(port)'\n"\
"# disable security locally\n"\
"c.NotebookApp.token = ''\n"\
"c.NotebookApp.password = ''"
# vim: noet ts=8

Просмотреть файл

@ -7,16 +7,13 @@ These are some API helper scripts for sanely managing a github org. For now this
## Credentials
Supplying credentials for execution is possible two ways:
1. provide a GitHub Personal Access Token (PAT) as the value of the environment
variable `GITHUB_PAT`. This is the prefered method.
1. Provide a GitHub PAT as the 2nd line of the file `.credentials`. This file is
listed in `.gitignore` to minimize the chance of committing it.
Supplying credentials for execution is done by passing a PAT token as the value
of the environment variable `GITHUB_TOKEN` (preferred) or `GITHUB_PAT`.
The recommended way to set `GITHUB_PAT` is via cli access to your password
The recommended way to set `GITHUB_TOKEN` is via cli access to your password
manager. For example, using [pass][pass]:
```bash
GITHUB_PAT=$(pass show myPAT) script args
GITHUB_TOKEN=$(pass show myPAT) script args
```
[pass]: https://www.passwordstore.org/
@ -60,13 +57,6 @@ feature. Use the ``--help`` option for more information.
### Audit logs
Sadly, the org audit log does not have an API, so we'll screen scrape a little.
#### audit-log-export.js
So here's a little bit of JavaScript you can run on any audit log page to export the data into a somewhat more digestible JSON format.
Example URL: https://github.com/orgs/acmecorp/audit-log
Yes, you have to go to the next page and re-run this to get another file.
#### hooks.py
Analyzes a list of audit log export files (from the JS script) for hook/service creation/deletion and provides a summary. Use it to show commonly used apps/services/webhooks across the org.
@ -74,6 +64,6 @@ Analyzes a list of audit log export files (from the JS script) for hook/service
Generate a list of empty (should be deleted) repositories as well as untouched repos (might need to be archived).
## License
This code is free software and licensed under an MPL-2.0 license. © 2015-2018 Fred Wenzel and others. For more information read the file ``LICENSE``.
This code is free software and licensed under an MPL-2.0 license. © 2015-2021 Fred Wenzel and others. For more information read the file ``LICENSE``.
[gd_url]: https://github.com/mozilla/geckodriver/releases

Просмотреть файл

@ -1,56 +0,0 @@
#!/usr/bin/env python
import json
import re
import sys
err = lambda s: sys.stderr.write('%s\n' % s)
if __name__ == '__main__':
if len(sys.argv) < 2:
err('Usage: %s audit-log.json audit-log-2.json ...' % sys.argv[0])
sys.exit(1)
# Load logs from file.
logs = []
for logfile in sys.argv[1:]:
logs.extend(json.loads(open(logfile).read()))
# Reduce to hook.{create,destroy}, sort.
logs = filter(lambda i: i['type'] in ('hook.create', 'hook.destroy'), logs)
logs.sort(key=lambda i: i['when'])
# List hooks and services per repo...
tally = {}
for log in logs:
# Find service/repo
m = re.search(r'(?:un)?installed (.+)\sfor ([\w/]+)', log['what'])
if not m:
err('No match for %s' % str(log))
continue
else:
service = m.group(1)
repo = m.group(2)
if log['type'] == 'hook.create':
if not tally.get(service):
tally[service] = set([repo])
else:
tally[service].add(repo)
elif log['type'] == 'hook.destroy':
if tally.get(service):
tally[service].discard(repo)
if not tally[service]: # Now empty
del tally[service]
# Else ignore.
# ... then count.
tally = [(name, len(repos)) for name, repos in tally.items()]
# Sort by amount of use, descending.
tally.sort(key=lambda i: i[1], reverse=True)
for service, usage in tally:
print '%s: %s' % (service, usage)

Просмотреть файл

@ -1,5 +1,6 @@
#!/usr/bin/env python
from __future__ import print_function
import ast
import json
import os
import re
@ -7,12 +8,12 @@ import sys
import time
from github_selenium import GitHub2FA, WebDriverException, webdriver
ORG = os.getenv('ORG', "mozilla")
URL = "https://github.com/organizations/{}/settings/audit-log".format(ORG)
ORG = os.getenv("ORG", "mozilla")
URL = f"https://github.com/organizations/{ORG}/settings/audit-log"
URL_TITLE = "Audit log"
GH_LOGIN = os.getenv('GH_LOGIN', "org_owner_login")
GH_PASSWORD = os.getenv('GH_PASSWORD', 'password')
HEADLESS = bool(os.getenv('HEADLESS', 'YES'))
GH_LOGIN = os.getenv("GH_LOGIN", "org_owner_login")
GH_PASSWORD = os.getenv("GH_PASSWORD", "password")
HEADLESS = bool(os.getenv("HEADLESS", "YES"))
# hack for legacy python support of token input (tokens often look like bad
# octal numbers and raise a syntax error)
@ -25,19 +26,20 @@ class Audit_Log_Download(GitHub2FA):
def __init__(self, *args, **kwargs):
# we have to create the profile before calling our superclass
fp = self._buildProfile()
kwargs['firefox_profile'] = fp
super(Audit_Log_Download, self).__init__(*args, **kwargs)
kwargs["firefox_profile"] = fp
super().__init__(*args, **kwargs)
def _buildProfile(self):
fx_profile = webdriver.FirefoxProfile()
fx_profile.set_preference("browser.download.folderList", 2)
fx_profile.set_preference("browser.download.manager.showWhenStarting",
False)
fx_profile.set_preference("browser.download.dir",
"/home/hwine/Downloads/geckodriver")
fx_profile.set_preference("browser.download.manager.showWhenStarting", False)
fx_profile.set_preference(
"browser.download.dir", "/home/hwine/Downloads/geckodriver"
)
fx_profile.set_preference("browser.download.panel.shown", True)
fx_profile.set_preference("browser.helperApps.neverAsk.saveToDisk",
"application/json")
fx_profile.set_preference(
"browser.helperApps.neverAsk.saveToDisk", "application/json"
)
return fx_profile
def get_values(self, selector):
@ -45,41 +47,42 @@ class Audit_Log_Download(GitHub2FA):
e = self.get_element(selector)
if e:
text = e.text
match = re.match(r'''\D+(?P<used>\S+)\D+(?P<purchased>\S+)''',
text)
match = re.match(r"""\D+(?P<used>\S+)\D+(?P<purchased>\S+)""", text)
if match:
d = match.groupdict()
used = float(d['used'].replace(',', ''))
purchased = float(d['purchased'].replace(',', ''))
used = float(d["used"].replace(",", ""))
purchased = float(d["purchased"].replace(",", ""))
else:
print("no element for '{}'".format(selector))
print(f"no element for '{selector}'")
used = purchased = None
return used, purchased
def download_file(self):
form_selector = 'div.select-menu-item:nth-child(1) > form:nth-child(1)'
form_selector = "div.select-menu-item:nth-child(1) > form:nth-child(1)"
# Simplest approach (??) is to build URL from form ourselves, and
# download. Avoids the system dialogs
form = self.get_element(form_selector)
results = {}
data = {}
allInputs = form.find_elements_by_xpath('.//INPUT')
allInputs = form.find_elements_by_xpath(".//INPUT")
for web_element in [x for x in allInputs if x.tag_name == "input"]:
name = web_element.get_attribute('name')
value = web_element.get_attribute('value')
name = web_element.get_attribute("name")
value = web_element.get_attribute("value")
data[name] = value
# build url
file_url = form.get_attribute('action')
results['action_url'] = file_url
results['body_data'] = data
file_url = form.get_attribute("action")
results["action_url"] = file_url
results["body_data"] = data
# make call & write to file
# Have to get there by clicking -- can't click directly as it's not
# visible.
self.get_element('.btn-sm').click() # The big export button
self.get_element('div.select-menu-item:nth-child(1) >'
' form:nth-child(1)'
' > button:nth-child(5)').click() # The JSON option
self.get_element(".btn-sm").click() # The big export button
self.get_element(
"div.select-menu-item:nth-child(1) >"
" form:nth-child(1)"
" > button:nth-child(5)"
).click() # The JSON option
# the clicks return faster than the download can happen, and the
# filename isn't deterministic. Try the easy way and wait 60 seconds.
# The "Export" button dims, but only while compiling the file, not
@ -92,9 +95,8 @@ if __name__ == "__main__":
# if all goes well, we quit. If not user is dropped into pdb while
# browser is still alive for introspection
# TODO put behind --debug option
print("Download latest audit log for organization {}".format(ORG))
print("Attempting login as '{}', please enter OTP when asked"
.format(GH_LOGIN))
print(f"Download latest audit log for organization {ORG}")
print(f"Attempting login as '{GH_LOGIN}', please enter OTP when asked")
print(" (if wrong, set GH_LOGIN & GH_PASSWORD in environtment properly)")
quit = True
try:
@ -105,14 +107,13 @@ if __name__ == "__main__":
driver = None
if not driver:
try:
token = input("token please: ")
token = ast.literal_eval(input("token please: "))
driver = Audit_Log_Download(headless=HEADLESS)
driver.login(GH_LOGIN, GH_PASSWORD, URL, URL_TITLE, token)
results = driver.download_file()
results['time'] = time.strftime('%Y-%m-%d %H:%M')
results["time"] = time.strftime("%Y-%m-%d %H:%M")
print(json.dumps(results))
print("File downloaded successfully - check your download"
" directory")
print("File downloaded successfully - check your download" " directory")
print("Any exception after this does not affect audit log")
print("But may leave your browser running")
except WebDriverException:
@ -120,13 +121,15 @@ if __name__ == "__main__":
print("Deep error - did browser crash?")
except ValueError as e:
quit = False
print("Navigation issue: {}".format(e.args[0]))
print(f"Navigation issue: {e.args[0]}")
if quit:
# logout first
driver.get_element('details.pl-2').click()
driver.get_element('.logout-form').click()
driver.get_element("details.pl-2").click()
driver.get_element(".logout-form").click()
driver.wait_for_page("The world's leading")
driver.quit()
else:
import pudb; pudb.set_trace() # noqa: E702
import pudb
pudb.set_trace() # noqa: E702

Просмотреть файл

@ -1,4 +1,3 @@
from datetime import datetime
import os
import time
@ -6,19 +5,23 @@ import time
from github3 import login
CREDENTIALS_FILE = '.credentials'
CREDENTIALS_FILE = ".credentials"
def get_token():
id = token = ''
token = os.environ.get("GITHUB_PAT", "")
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GITHUB_PAT")
if not token:
with open(CREDENTIALS_FILE, 'r') as cf:
id = cf.readline().strip()
token = cf.readline().strip()
raise KeyError(
"""ERROR - GitHub token must be provided via environment"""
""" variable "GITHUB_TOKEN" or "GITHUB_PAT"."""
""" Please delete any old ".credentials" file."""
)
return token
# get_token()
def get_github3_client():
token = get_token()
gh = login(token=token)
@ -28,8 +31,8 @@ def get_github3_client():
def sleep_if_rate_limited(gh, verbose=False):
rates = gh.rate_limit()
if not rates['resources']['search']['remaining']:
reset_epoch = rates['resources']['search']['reset']
if not rates["resources"]["search"]["remaining"]:
reset_epoch = rates["resources"]["search"]["reset"]
reset_dt, now = datetime.utcfromtimestamp(reset_epoch), datetime.utcnow()
@ -37,6 +40,13 @@ def sleep_if_rate_limited(gh, verbose=False):
sleep_secs = (reset_dt - now).seconds + 1
if verbose:
print('sleeping for', sleep_secs, 'got rate limit', rates['resources']['search'])
print(
(
"sleeping for",
sleep_secs,
"got rate limit",
rates["resources"]["search"],
)
)
time.sleep(sleep_secs)

Просмотреть файл

@ -1,8 +1,7 @@
#!/usr/bin/env python
"""
Close PRs on repositories where the master is not on github.
"""Close PRs on repositories where the master is not on github.
Provide a closing comment, and print the lock URL if desired
Provide a closing comment, and print the lock URL if desired
"""
import argparse
@ -11,19 +10,21 @@ import yaml
from client import get_github3_client
DEFAULT_MESSAGE = 'We do not use Pull Requests on this repo. Please see ' \
'CONTRIBUTING or ReadMe file.'
DEFAULT_CONFIG = 'close_pull_requests.yaml'
DEFAULT_MESSAGE = (
"We do not use Pull Requests on this repo. Please see "
"CONTRIBUTING or ReadMe file."
)
DEFAULT_CONFIG = "close_pull_requests.yaml"
logger = logging.getLogger(__name__)
exit_code = 0
def update_exit_code(new_code):
""" Update global exit_code, following rules.
"""Update global exit_code, following rules.
Current rule is only update if the new value signifies a
"more severe" error (higher integer value)
Current rule is only update if the new value signifies a "more
severe" error (higher integer value)
"""
global exit_code
exit_code = max(exit_code, new_code)
@ -38,35 +39,45 @@ def lock_pr(repo, number):
return success
def close_prs(gh, organization=None, repository=None,
message=None, lock=False, close=False):
def close_prs(
gh, organization=None, repository=None, message=None, lock=False, close=False
):
if message is None:
message = DEFAULT_MESSAGE
try:
repo = gh.repository(organization, repository)
logger.debug("Checking for PRs in %s", repo.name)
for pr in repo.pull_requests(state='open'):
logger.debug("Examining PR %s for %s/%s", pr.number,
organization, repository)
for pr in repo.pull_requests(state="open"):
logger.debug(
"Examining PR %s for %s/%s", pr.number, organization, repository
)
if close:
pr.create_comment(message)
pr.close()
logger.info("Closed PR %s for %s/%s", pr.number,
organization, repository)
logger.info(
"Closed PR %s for %s/%s", pr.number, organization, repository
)
if lock:
success = lock_pr(repo, pr.number)
if not success:
print("Lock PR manually: "
"https://github.com/%s/%s/pull/%s" %
(organization, repository, pr.number))
print(
"Lock PR manually: "
"https://github.com/%s/%s/pull/%s"
% (organization, repository, pr.number)
)
else:
print("PR %s open for %s/%s at: "
"https://github.com/%s/%s/pull/%s" % (pr.number,
organization,
repository,
organization,
repository,
pr.number))
print(
"PR %s open for %s/%s at: "
"https://github.com/%s/%s/pull/%s"
% (
pr.number,
organization,
repository,
organization,
repository,
pr.number,
)
)
else:
logger.debug("no open PR's in %s!", repo.name)
except AttributeError:
@ -76,28 +87,34 @@ def close_prs(gh, organization=None, repository=None,
def close_configured_prs(gh, config_file, dry_run=False):
config = []
with open(config_file, 'rb') as yaml_file:
with open(config_file, "rb") as yaml_file:
config = yaml.safe_load(yaml_file)
for repository in config:
if dry_run:
repository['lock'] = False
repository['close'] = False
repository["lock"] = False
repository["close"] = False
close_prs(gh, **repository)
def parse_args():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--only', help="org/repo to use instead of config")
parser.add_argument('--message', help="comment to add (default '%s')" %
DEFAULT_MESSAGE, default=None)
parser.add_argument('--close', action='store_true', help="Close PR")
parser.add_argument('--lock', action='store_true', help="Lock PR")
parser.add_argument('--debug', help="include github3 output",
action='store_true')
parser.add_argument('--config', help="read configs for projects (default "
"'%s')" % DEFAULT_CONFIG, default=DEFAULT_CONFIG)
parser.add_argument('--dry-run', action='store_true',
help='Just show, regardless of config')
parser.add_argument("--only", help="org/repo to use instead of config")
parser.add_argument(
"--message",
help="comment to add (default '%s')" % DEFAULT_MESSAGE,
default=None,
)
parser.add_argument("--close", action="store_true", help="Close PR")
parser.add_argument("--lock", action="store_true", help="Lock PR")
parser.add_argument("--debug", help="include github3 output", action="store_true")
parser.add_argument(
"--config",
help="read configs for projects (default " "'%s')" % DEFAULT_CONFIG,
default=DEFAULT_CONFIG,
)
parser.add_argument(
"--dry-run", action="store_true", help="Just show, regardless of config"
)
return parser.parse_args()
@ -105,20 +122,26 @@ def main():
args = parse_args()
if args.debug:
logger.setLevel(logging.DEBUG)
logging.getLogger('github3').setLevel(logging.DEBUG)
logging.getLogger("github3").setLevel(logging.DEBUG)
gh = get_github3_client()
me = gh.me()
logger.debug("I'm %s (%s)", me.name, me.login)
if args.only:
org, repo = args.only.split('/')
close_prs(gh, organization=org, repository=repo, close=args.close,
lock=args.lock, message=args.message)
org, repo = args.only.split("/")
close_prs(
gh,
organization=org,
repository=repo,
close=args.close,
lock=args.lock,
message=args.message,
)
else:
close_configured_prs(gh, args.config, args.dry_run)
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
logging.getLogger('github3').setLevel(logging.ERROR)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
logging.getLogger("github3").setLevel(logging.ERROR)
main()
raise SystemExit(exit_code)

Просмотреть файл

@ -38,4 +38,3 @@
close: True
lock: True
message: (Automated Close) Please do not file PRs here, see https://mozilla-version-control-tools.readthedocs.io/en/latest/devguide/contributing.html

Просмотреть файл

@ -1,4 +1,3 @@
__doc__ = """Searches for code across multiple github orgs."""
import argparse
@ -12,26 +11,35 @@ from client import (
DEFAULT_ORGS = [
'mozilla',
'mozilla-conduit',
'mozilla-platform-ops',
'mozilla-releng',
'mozilla-services',
'taskcluster',
"mozilla",
"mozilla-conduit",
"mozilla-platform-ops",
"mozilla-releng",
"mozilla-services",
"taskcluster",
]
def parse_args():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("query", type=str,
help='code search query. syntax at https://help.github.com/articles/searching-code/')
parser.add_argument("--orgs", default=DEFAULT_ORGS, nargs='*',
help='organizations to search (defaults to {})'.format(DEFAULT_ORGS))
parser.add_argument("--json",
help='path to output json results', type=str, default=None)
parser.add_argument("--verbose", action='store_true',
help='print logins for all changes')
parser.add_argument(
"query",
type=str,
help="code search query. syntax at https://help.github.com/articles/searching-code/",
)
parser.add_argument(
"--orgs",
default=DEFAULT_ORGS,
nargs="*",
help=f"organizations to search (defaults to {DEFAULT_ORGS})",
)
parser.add_argument(
"--json", help="path to output json results", type=str, default=None
)
parser.add_argument(
"--verbose", action="store_true", help="print logins for all changes"
)
return parser.parse_args()
@ -40,17 +48,21 @@ def main():
args = parse_args()
gh = get_github3_client()
json_fout = jsonstreams.Stream(jsonstreams.Type.array, args.json, indent=4) if args.json else None
json_fout = (
jsonstreams.Stream(jsonstreams.Type.array, args.json, indent=4)
if args.json
else None
)
for org in args.orgs:
full_query = 'org:{} {}'.format(org, args.query)
full_query = f"org:{org} {args.query}"
if args.verbose:
print('searching with query {}'.format(full_query))
print(f"searching with query {full_query}")
sleep_if_rate_limited(gh, verbose=args.verbose)
print("{0:<16}{1:<32}{2:<64}".format('org', 'repo', 'file path'))
print("{:<16}{:<32}{:<64}".format("org", "repo", "file path"))
for result in gh.search_code(full_query):
print("{0:<16}{1.repository.name:<32}{1.path:<64}".format(org, result))
@ -62,5 +74,5 @@ def main():
json_fout.close()
if __name__ == '__main__':
if __name__ == "__main__":
main()

Просмотреть файл

@ -4,8 +4,9 @@ import sys
from client import get_github3_client
from github3.exceptions import UnprocessableResponseBody
def get_files(repo, directory):
""" Get the files from this repo
"""Get the files from this repo.
The interface on this for github3.py version 1.0.0a4 is not yet
stable, so some coding-by-coincidence is used.
@ -16,28 +17,29 @@ def get_files(repo, directory):
# response.body contains json of the directory contents as a list of
# dictionaries. The calling code wants a dictionary with file
# names as keys.
names = dict(((x['name'], None) for x in response.body))
names = {x["name"]: None for x in response.body}
else:
raise Exception("github3.py behavior changed")
return names
if __name__ == '__main__':
if __name__ == "__main__":
gh = get_github3_client()
# Divvy up into repositories that need/do not need a CONTRIBUTING file.
good_repos = []
bad_repos = []
repos = gh.organization('mozilla').repositories(type='sources')
repos = gh.organization("mozilla").repositories(type="sources")
for repo in repos:
# All files in this repo's default branch.
# {'filename.md': Content(), 'filename2.txt': Contents(), ...}
files = get_files(repo, '/')
files = get_files(repo, "/")
if files:
contrib_files = [f for f in files.keys() if
f.startswith('CONTRIBUTING')]
contrib_files = [
f for f in list(files.keys()) if f.startswith("CONTRIBUTING")
]
else:
contrib_files = None
@ -46,17 +48,17 @@ if __name__ == '__main__':
else:
bad_repos.append(repo.name)
sys.stderr.write('.')
sys.stderr.write(".")
good_repos.sort()
print
print 'The following repos HAVE a CONTRIBUTING file:'
print()
print("The following repos HAVE a CONTRIBUTING file:")
for r in good_repos:
print r
print(r)
bad_repos.sort()
print
print
print 'The following repos DO NOT HAVE a CONTRIBUTING file:'
print()
print()
print("The following repos DO NOT HAVE a CONTRIBUTING file:")
for r in bad_repos:
print r
print(r)

Просмотреть файл

@ -1,7 +1,5 @@
#!/usr/bin/env python
"""
Report on Service & Web hooks for organization.
"""
"""Report on Service & Web hooks for organization."""
_epilog = """
To avoid issues with large organizations and API rate limits, the data
for each organization is cached in a tinydb database named <org>.db.
@ -11,25 +9,23 @@ are not handled. Manually remove the database to force a full query.
import argparse
import client
import logging
import urlparse
import urllib.parse
import yaml
import tinydb
"""
Lore: swapping hook.test for hook.ping will cause repetition of the
actions. In particular, a number of repos post to IRC channels
and/or bugs on commits, so expect comments to that effect.
"""
# Lore: swapping hook.test for hook.ping will cause repetition of the
# actions. In particular, a number of repos post to IRC channels
# and/or bugs on commits, so expect comments to that effect.
logger = logging.getLogger(__name__)
def wait_for_karma(gh, min_karma=25, msg=None):
while gh:
core = gh.rate_limit()['resources']['core']
if core['remaining'] < min_karma:
core = gh.rate_limit()["resources"]["core"]
if core["remaining"] < min_karma:
now = time.time()
nap = max(core['reset'] - now, 0.1)
nap = max(core["reset"] - now, 0.1)
logger.info("napping for %s seconds", nap)
if msg:
logger.info(msg)
@ -48,24 +44,26 @@ def get_hook_name(hook):
# several per repo. The unique part is the hook['config']['url'], which
# may contain sensitive info (including basic login data), so just
# grab scheme, hostname, and port.
if hook['name'] != "web":
name = hook['name']
if hook["name"] != "web":
name = hook["name"]
else:
url = hook['config']['url']
parts = urlparse.urlparse(url)
url = hook["config"]["url"]
parts = urllib.parse.urlparse(url)
# port can be None, which prints funny, but is good enough for
# identification.
name = "%s://%s:%s" % (parts.scheme, parts.hostname, parts.port)
name = f"{parts.scheme}://{parts.hostname}:{parts.port}"
return name
def report_hooks(gh, org, active_only=False, unique_only=False,
do_ping=False, yaml_out=False):
def report_hooks(
gh, org, active_only=False, unique_only=False, do_ping=False, yaml_out=False
):
org_handle = gh.organization(org)
with tinydb.TinyDB('{}.db'.format(org)) as db:
with tinydb.TinyDB(f"{org}.db") as db:
q = tinydb.Query()
org_struct = org_handle.as_dict()
repo_list = []
org_struct['repo_list'] = repo_list
org_struct["repo_list"] = repo_list
unique_hooks = set()
msg = "Active" if active_only else "All"
for repo in org_handle.repositories():
@ -75,20 +73,20 @@ def report_hooks(gh, org, active_only=False, unique_only=False,
have_data = len(l) == 1
if have_data:
# already have data
logger.debug("Already have data for {}".format(repo.name))
logger.debug(f"Already have data for {repo.name}")
# load existing data
repo_struct = l[0]
hook_list = repo_struct['hook_list']
hook_list = repo_struct["hook_list"]
repo_hooks = {get_hook_name(x) for x in hook_list}
else:
wait_for_karma(gh, 100, msg="waiting at {}".format(repo.name))
wait_for_karma(gh, 100, msg=f"waiting at {repo.name}")
repo_struct = repo.as_dict()
hook_list = []
repo_struct['hook_list'] = hook_list
repo_struct["hook_list"] = hook_list
repo_list.append(repo_struct)
ping_attempts = ping_fails = 0
for hook in repo.hooks():
wait_for_karma(gh, 100, msg="waiting at hooks() for {}".format(repo.name))
wait_for_karma(gh, 100, msg=f"waiting at hooks() for {repo.name}")
hook_struct = hook.as_dict()
hook_list.append(hook_struct)
name = get_hook_name(hook)
@ -98,36 +96,47 @@ def report_hooks(gh, org, active_only=False, unique_only=False,
ping_attempts += 1
if not hook.ping():
ping_fails += 1
logger.warning('Ping failed for %s', name)
logger.warning("Ping failed for %s", name)
if repo_hooks and not unique_only:
print("%s hooks for %s:" % (msg, repo.name))
print(f"{msg} hooks for {repo.name}:")
if do_ping:
print(" pinged %d (%d failed)" % (
ping_attempts, ping_fails))
print(" pinged %d (%d failed)" % (ping_attempts, ping_fails))
for h in repo_hooks:
print(" {:s}".format(h))
print(f" {h:s}")
unique_hooks = unique_hooks.union(repo_hooks)
# now that we're done with this repo, persist the data
if not have_data:
db.insert(repo_struct)
if yaml_out:
print(yaml.safe_dump([org_struct, ]))
print(
yaml.safe_dump(
[
org_struct,
]
)
)
elif unique_only and unique_hooks:
print("%s hooks for org %s" % (msg, org))
print(f"{msg} hooks for org {org}")
for h in unique_hooks:
print(h)
def parse_args():
parser = argparse.ArgumentParser(description=__doc__, epilog=_epilog)
parser.add_argument("org", help='Organization', default=['mozilla'],
nargs='*')
parser.add_argument("--active", action='store_true',
help="Show active hooks only (not for cached repositories)")
parser.add_argument("--unique", help="Show unique hook names only",
action='store_true')
parser.add_argument("--ping", action="store_true",
help="Ping all hooks (not for cached repositories)")
parser.add_argument("org", help="Organization", default=["mozilla"], nargs="*")
parser.add_argument(
"--active",
action="store_true",
help="Show active hooks only (not for cached repositories)",
)
parser.add_argument(
"--unique", help="Show unique hook names only", action="store_true"
)
parser.add_argument(
"--ping",
action="store_true",
help="Ping all hooks (not for cached repositories)",
)
parser.add_argument("--yaml", help="Yaml ouput only", action="store_true")
return parser.parse_args()
@ -139,9 +148,9 @@ def main():
report_hooks(gh, org, args.active, args.unique, args.ping, args.yaml)
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
logging.getLogger('github3').setLevel(logging.WARNING)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
logging.getLogger("github3").setLevel(logging.WARNING)
try:
main()
except KeyboardInterrupt:

Просмотреть файл

@ -1,7 +1,6 @@
#!/usr/bin/env python
# PYTHON_ARGCOMPLETE_OK
"""Report Basic info about orgs."""
from __future__ import print_function
# additional help text
_epilog = """
@ -40,12 +39,10 @@ def show_info(gh, org_name, show_owners=False, show_emails=False, show_json=Fals
jsonl_out(orgd)
return
v4decoded = "{:03}:{}{}".format(len(orgd["type"]), orgd["type"], str(org.id))
v4encoded = base64.b64encode(v4decoded)
v4encoded = base64.b64encode(bytes(v4decoded, "utf-8"))
print("{:>15}: {!s} ({})".format("Name", org.name or org_name, orgd["login"]))
print("{:>15}: {!s}".format("API v3 id", org.id))
print(
"{:>15}: {!s}".format("API v4 id", "{} ({})".format(v4encoded, v4decoded))
)
print("{:>15}: {!s}".format("API v4 id", f"{v4encoded} ({v4decoded})"))
print("{:>15}: {!s}".format("contact", org.email))
print("{:>15}: {!s}".format("billing", orgd["billing_email"]))
print(
@ -69,7 +66,7 @@ def show_info(gh, org_name, show_owners=False, show_emails=False, show_json=Fals
email = " " + (owner.email or "<email hidden>")
else:
email = ""
print(" {} ({}{})".format(name, owner.login, email))
print(f" {name} ({owner.login}{email})")
except Exception as e:
logger.error("Error %s obtaining data for org '%s'", str(e), str(org))
finally:
@ -98,7 +95,10 @@ def parse_args():
help="Only output your org names for which you're an owner",
)
parser.add_argument(
"orgs", nargs="*", help="github organizations to check (defaults to " "mozilla)"
"orgs",
nargs="*",
help="github organizations to check (defaults to " "mozilla)",
default=["mozilla"],
)
argcomplete.autocomplete(parser)
args = parser.parse_args()
@ -121,7 +121,7 @@ def parse_args():
# belongs in authenticated user
class MyOrganizationsIterator(github3.structs.GitHubIterator):
def __init__(self, me):
super(MyOrganizationsIterator, self).__init__(
super().__init__(
count=-1, # get all
url=me.session.base_url + "/user/orgs",
cls=github3.orgs.Organization,
@ -189,7 +189,7 @@ def main():
newline = ""
for org in args.orgs:
if len(args.orgs) > 1 and not args.json:
print("{}Processing org {}".format(newline, org))
print(f"{newline}Processing org {org}")
newline = "\n"
show_info(gh, org, args.owners, args.email, args.json)
except github3.exceptions.ForbiddenError as e:

19
lfs.py
Просмотреть файл

@ -1,8 +1,7 @@
#!/usr/bin/env python
"""
Extract current LFS stats from GitHub web UI
"""
from __future__ import print_function
"""Extract current LFS stats from GitHub web UI."""
import ast
import json
import os
import re
@ -38,11 +37,11 @@ class LFS_Usage(GitHub2FA):
used = float(d["used"].replace(",", ""))
purchased = float(d["purchased"].replace(",", ""))
else:
print("no match for '{}'".format(selector))
print(" for text '{}'".format(text))
print(f"no match for '{selector}'")
print(f" for text '{text}'")
used = purchased = None
else:
print("no element for '{}'".format(selector))
print(f"no element for '{selector}'")
used = purchased = None
return used, purchased
@ -80,7 +79,7 @@ def parse_args():
def main():
args = parse_args()
print("Obtain current LFS billing info")
print("Attempting login as '{}', please enter OTP when asked".format(GH_LOGIN))
print(f"Attempting login as '{GH_LOGIN}', please enter OTP when asked")
print(" (if wrong, set GH_LOGIN & GH_PASSWORD in environtment properly)")
quit = not args.debug
try:
@ -95,7 +94,7 @@ def main():
opts = Options()
opts.log.level = "trace"
try:
token = input("token please: ")
token = ast.literal_eval(input("token please: "))
driver = LFS_Usage(headless=args.headless, options=opts)
driver.login(GH_LOGIN, GH_PASSWORD, URL, "Billing", token)
results = driver.get_usage()
@ -106,7 +105,7 @@ def main():
print("Deep error - did browser crash?")
except ValueError as e:
quit = not args.debug
print("Navigation issue: {}".format(e.args[0]))
print(f"Navigation issue: {e.args[0]}")
if quit:
driver.quit()

Просмотреть файл

@ -1,8 +1,6 @@
#!/usr/bin/env python
"""
Report and manage pending org invitations
"""
from __future__ import print_function
"""Report and manage pending org invitations."""
# additional help text
_epilog = """
@ -27,23 +25,29 @@ import logging # NOQA
import arrow # NOQA
from client import get_github3_client # NOQA
# hack until invitations are supported upstream
import github3 # NOQA
from github3.exceptions import ForbiddenError # NOQA
if not hasattr(github3.orgs.Organization, 'invitations'):
raise NotImplementedError("Your version of github3.py does not support "
"invitations. Try "
"https://github.com/hwine/github3.py/tree/invitations") # NOQA
if (1,3,0) > github3.__version_info__:
raise NotImplementedError("Your version of github3.py does not support "
"collaborator invitations. Version '1.3.0' or later is known to work.")
if not hasattr(github3.orgs.Organization, "invitations"):
raise NotImplementedError(
"Your version of github3.py does not support "
"invitations. Try "
"https://github.com/hwine/github3.py/tree/invitations"
) # NOQA
if (1, 3, 0) > github3.__version_info__:
raise NotImplementedError(
"Your version of github3.py does not support "
"collaborator invitations. Version '1.3.0' or later is known to work."
)
logger = logging.getLogger(__name__)
def get_cutoff_time(cutoff_delta):
k, v = cutoff_delta.split('=', 2)
k, v = cutoff_delta.split("=", 2)
args = {k: int(v)}
ok_after = arrow.now().replace(**args)
return ok_after
@ -62,21 +66,24 @@ def check_invites(gh, org_name, cancel=False, cutoff_delta="weeks=-2"):
line_end = ": " if cancel else "\n"
if extended_at < cutoff_time:
context = invite.as_dict()
context['ago'] = extended_at.humanize()
print('{login} ({email}) was invited {ago} by '
'{inviter[login]}'.format(**context),
end=line_end)
context["ago"] = extended_at.humanize()
print(
"{login} ({email}) was invited {ago} by "
"{inviter[login]}".format(**context),
end=line_end,
)
if cancel:
success = org.remove_membership(invite.id)
if success:
print("Cancelled")
else:
print("FAILED to cancel")
logger.warning("Couldn't cancel invite for {login} "
"from {created_at}".format(**context))
logger.warning(
"Couldn't cancel invite for {login} "
"from {created_at}".format(**context)
)
except ForbiddenError:
logger.error("You don't have 'admin:org' permissions for org '%s'",
org_name)
logger.error("You don't have 'admin:org' permissions for org '%s'", org_name)
else:
# now handle collaborator invitations (GH-57)
for repo in org.repositories():
@ -89,13 +96,15 @@ def check_invites(gh, org_name, cancel=False, cutoff_delta="weeks=-2"):
line_end = ": " if cancel else "\n"
if extended_at < cutoff_time:
context = invite.as_dict()
context['ago'] = extended_at.humanize()
context['repo'] = repo.name
context['inviter'] = invite.inviter.login
context['invitee'] = invite.invitee.login
print('{invitee} was invited to {repo} {ago} by '
'{inviter} for {permissions} access.'.format(**context),
end=line_end)
context["ago"] = extended_at.humanize()
context["repo"] = repo.name
context["inviter"] = invite.inviter.login
context["invitee"] = invite.invitee.login
print(
"{invitee} was invited to {repo} {ago} by "
"{inviter} for {permissions} access.".format(**context),
end=line_end,
)
if cancel:
# Deletion not directly supported, so hack url &
# use send delete verb directly
@ -105,31 +114,49 @@ def check_invites(gh, org_name, cancel=False, cutoff_delta="weeks=-2"):
print("Cancelled")
else:
print("FAILED to cancel")
logger.warning("Couldn't cancel invite for {login} "
"from {created_at}".format(**context))
except (github3.exceptions.NotFoundError,
github3.exceptions.ConnectionError) as e:
logger.warning(
"Couldn't cancel invite for {login} "
"from {created_at}".format(**context)
)
except (
github3.exceptions.NotFoundError,
github3.exceptions.ConnectionError,
) as e:
# just report
logger.warning("Got 404 for invitation in {}, may be unhandled inviations. '{}'".format(repo.name,
str(e)))
logger.warning(
"Got 404 for invitation in {}, may be unhandled inviations. '{}'".format(
repo.name, str(e)
)
)
def parse_args():
# from
# https://stackoverflow.com/questions/18462610/argumentparser-epilog-and-description-formatting-in-conjunction-with-argumentdef
class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter):
class CustomFormatter(
argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter
):
pass
parser = argparse.ArgumentParser(description=__doc__,
epilog=_epilog, formatter_class=CustomFormatter)
parser.add_argument('--cancel', action='store_true',
help='Cancel stale invitations')
parser.add_argument('--cutoff', help='When invitations go stale '
'(arrow replace syntax; default "weeks=-2")',
default="weeks=-2")
parser.add_argument("orgs", nargs='*', default=['mozilla', ],
help='github organizations to check (defaults to '
'mozilla)')
parser = argparse.ArgumentParser(
description=__doc__, epilog=_epilog, formatter_class=CustomFormatter
)
parser.add_argument(
"--cancel", action="store_true", help="Cancel stale invitations"
)
parser.add_argument(
"--cutoff",
help="When invitations go stale " '(arrow replace syntax; default "weeks=-2")',
default="weeks=-2",
)
parser.add_argument(
"orgs",
nargs="*",
default=[
"mozilla",
],
help="github organizations to check (defaults to " "mozilla)",
)
# make sure arrow is happy with the cutoff syntax
args = parser.parse_args()
try:
@ -145,12 +172,12 @@ def main():
gh = get_github3_client()
for org in args.orgs:
if len(args.orgs) > 1:
print("Processing org {}".format(org))
print(f"Processing org {org}")
check_invites(gh, org, args.cancel, args.cutoff)
if __name__ == '__main__':
logging.basicConfig(level=logging.WARN, format='%(asctime)s %(message)s')
if __name__ == "__main__":
logging.basicConfig(level=logging.WARN, format="%(asctime)s %(message)s")
try:
main()
except KeyboardInterrupt:

Просмотреть файл

@ -9,63 +9,80 @@ import requests
from client import get_token
CACHEFILE = 'cache/mozilla_all_repos.json'
CACHEFILE = "cache/mozilla_all_repos.json"
def parse_timestamp(tst):
return datetime.strptime(tst, '%Y-%m-%dT%H:%M:%SZ')
return datetime.strptime(tst, "%Y-%m-%dT%H:%M:%SZ")
if __name__ == '__main__':
if __name__ == "__main__":
headers = {
'Accept': 'application/vnd.github.moondragon+json',
'Authorization': 'token %s' % get_token()
"Accept": "application/vnd.github.moondragon+json",
"Authorization": "token %s" % get_token(),
}
if os.path.exists(CACHEFILE):
repos = json.loads(open(CACHEFILE, 'r').read())
print ('Found cached repository list. Delete %s if you want a new '
'one.\n' % CACHEFILE)
repos = json.loads(open(CACHEFILE).read())
print(
"Found cached repository list. Delete %s if you want a new "
"one.\n" % CACHEFILE
)
else:
repos = []
repos_api = 'https://api.github.com/orgs/%s/repos' % 'mozilla'
repos_api = "https://api.github.com/orgs/%s/repos" % "mozilla"
while True:
resp = requests.get(repos_api, headers=headers)
repos += resp.json()
# Next page.
next_match = re.search(r'<([^>]+)>; rel="next"',
resp.headers['Link'])
next_match = re.search(r'<([^>]+)>; rel="next"', resp.headers["Link"])
if not next_match:
break
else:
repos_api = next_match.group(1)
open(CACHEFILE, 'w').write(json.dumps(repos))
open(CACHEFILE, "w").write(json.dumps(repos))
# Find small/empty repos older than a month.
SMALL_MINAGE = 31
small_repos = filter(lambda r: (
r['size'] < 50 and
r['open_issues_count'] == 0 and
parse_timestamp(r['updated_at']) + timedelta(days=SMALL_MINAGE) < datetime.now()),
repos)
small_repos.sort(key=lambda r: r['name'])
print '## %s small/empty repositories older than %s days' % (
len(small_repos), SMALL_MINAGE)
small_repos = [
r
for r in repos
if (
r["size"] < 50
and r["open_issues_count"] == 0
and parse_timestamp(r["updated_at"]) + timedelta(days=SMALL_MINAGE)
< datetime.now()
)
]
small_repos.sort(key=lambda r: r["name"])
print(
"## {} small/empty repositories older than {} days".format(
len(small_repos), SMALL_MINAGE
)
)
for repo in small_repos:
print repo['name'], ':', repo['size'], '(%s)' % repo['updated_at']
print(repo["name"], ":", repo["size"], "(%s)" % repo["updated_at"])
print '\n\n'
print("\n\n")
# Find recently untouched repos.
UNTOUCHED_MINAGE = 2 * 365
old_repos = filter(lambda r: (
parse_timestamp(r['updated_at']) +
timedelta(days=(UNTOUCHED_MINAGE)) < datetime.now()), repos)
old_repos.sort(key=lambda r: r['name'])
print '## %s repos touched less recently than %s days ago.' % (
len(old_repos), UNTOUCHED_MINAGE)
old_repos = [
r
for r in repos
if (
parse_timestamp(r["updated_at"]) + timedelta(days=(UNTOUCHED_MINAGE))
< datetime.now()
)
]
old_repos.sort(key=lambda r: r["name"])
print(
"## {} repos touched less recently than {} days ago.".format(
len(old_repos), UNTOUCHED_MINAGE
)
)
for repo in old_repos:
print repo['name'], ':', repo['updated_at']
print(repo["name"], ":", repo["updated_at"])

Просмотреть файл

@ -1,12 +1,9 @@
#!/usr/bin/env python
"""
Print all owner activity from an audit log
"""
from __future__ import print_function
"""Print all owner activity from an audit log."""
import argparse
import logging
import subprocess
import subprocess # nosec
from client import get_github3_client
import github3
@ -14,19 +11,25 @@ import github3
logger = logging.getLogger(__name__)
def get_org_access(login, json_file):
cmd = ['jq', '-rc',
""" .[] | select(.actor == "{}") | select(.action|test("org.")) | [(.created_at/1000 | todate), .actor, .action] | @csv """.format(login)]
with open(json_file, 'r') as json:
csv_output = subprocess.check_output(cmd, shell=False,
stderr=subprocess.STDOUT, stdin=json)
cmd = [
"jq",
"-rc",
""" .[] | select(.actor == "{}") | select(.action|test("org.")) | [(.created_at/1000 | todate), .actor, .action] | @csv """.format(
login
),
]
with open(json_file) as json:
csv_output = subprocess.check_output( # nosec
cmd, shell=False, stderr=subprocess.STDOUT, stdin=json
)
return csv_output
def process_org(gh, args):
"""
Get owners for specified org, then output actions done by them that
required org owner permissions.
"""
"""Get owners for specified org, then output actions done by them that
required org owner permissions."""
org = gh.organization(args.org)
for l in org.members(role="admin"):
csv = get_org_access(l.login, args.audit_file)
@ -35,10 +38,8 @@ def process_org(gh, args):
def parse_args():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--org", default='mozilla',
help='GitHub org to process')
parser.add_argument("audit_file",
help='JSON audit log to process')
parser.add_argument("--org", default="mozilla", help="GitHub org to process")
parser.add_argument("audit_file", help="JSON audit log to process")
args = parser.parse_args()
return args
@ -49,6 +50,6 @@ def main():
process_org(gh, args)
if __name__ == '__main__':
logging.basicConfig(level=logging.WARN, format='%(asctime)s %(message)s')
if __name__ == "__main__":
logging.basicConfig(level=logging.WARN, format="%(asctime)s %(message)s")
main()

1467
poetry.lock сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

37
postBuild Normal file
Просмотреть файл

@ -0,0 +1,37 @@
#!/bin/bash
#
# There are two different environments we need to support:
# - kernel -- for anything needed by the notebook code itself (py2)
# - notebook -- for anything used by the Jupyter framework (py3)
#
# configure to allow conda from terminal (we don't have an interactive one
# during docker image build, so don't have .bashrc)
conda init bash
. .bashrc
# We need ipywidgets for our notebook
# (could be moved to requirements.txt, but want to keep that CLI only for now)
conda activate kernel
conda install -y -c conda-forge ipywidgets
# We need to install the rest into the server's environment.
conda activate notebook
conda install -y -c conda-forge jupyter_contrib_nbextensions
### ones we need for sure
##- "initialization cells" aka 'init_cell'
jupyter nbextension enable init_cell/main
jupyter nbextension enable collapsible_headings/main
##
### maybe add
##- "Select CodeMirror Keymap" aka "select_keymap"
##- "Tree Filter" aka "tree-filter"
##- "Codefolding in Editor" aka "codefolding"
##- "Collapsible Headings" aka "collapsible_headings"
#jupyter labextension list --help-all
#jupyter labextension enable init_cell
##
### debug help
##- "Nbextensions edit menu item" aka "nbextensions_configurator"
##- "contrib_nbextensions_help_item"

21
pyproject.toml Normal file
Просмотреть файл

@ -0,0 +1,21 @@
[tool.poetry]
name = "github-org-scripts"
version = "0.1.0"
description = "Misc scripts"
authors = ["Hal Wine <hwine@mozilla.com>"]
license = "MPL-2.0"
[tool.poetry.dependencies]
python = "^3.8"
jsonstreams = "=0.4.1"
PyYaml = "=5.1.2"
tinydb = "=3.2.1"
github_selenium = { git = "https://github.com/mozilla/github_selenium.git" }
arrow = "^0.12.1"
#"github3.py" = { git = "https://github.com/hwine/github3.py.git", branch = "invitations" }
"github3.py" = "1.0.0a4"
jupyter = "^1.0.0"
argcomplete = "^1.12.3"
[tool.poetry.dev-dependencies]
pytest = "^6.2.3"

Просмотреть файл

@ -1,5 +1,4 @@
#!/usr/bin/env python
# coding: utf-8
# ## Remove a login from org
#
@ -13,18 +12,22 @@
# Not yet handled:
# - keeping track of when they are team maintainers
from __future__ import print_function
import logging
import argparse
import github3
from client import get_github3_client
# hack until https://github.com/sigmavirus24/github3.py/pull/675 merged
from github3.exceptions import ForbiddenError # NOQA
if not hasattr(github3.orgs.Organization, 'invitations'):
raise NotImplementedError("Your version of github3.py does not support "
"invitations. Try "
"https://github.com/hwine/github3.py/tree/invitations") # NOQA
if not hasattr(github3.orgs.Organization, "invitations"):
raise NotImplementedError(
"Your version of github3.py does not support "
"invitations. Try "
"https://github.com/hwine/github3.py/tree/invitations"
) # NOQA
logger = logging.getLogger(__name__)
@ -33,41 +36,50 @@ def remove_login_from_org(login, org_name):
org = gh.organization(org_name)
user = org.membership(login)
if user:
if user['role'] in ['admin']:
logger.warn("manually change {} to a member first".format(login))
if user["role"] in ["admin"]:
logger.warn(f"manually change {login} to a member first")
else:
if not dry_run:
if org.remove_membership(login):
logger.info("removed {} from {}".format(login, org_name))
logger.info(f"removed {login} from {org_name}")
else:
logger.error("ERROR removing {} from {}".format(login,
org_name))
logger.error(f"ERROR removing {login} from {org_name}")
# remove any outside collaborator settings
# HACK - no method, so hack the URL and send it directly
oc_url = org._json_data['issues_url'].replace('issues',
'outside_collaborators')
delete_url = oc_url + '/' + login
oc_url = org._json_data["issues_url"].replace("issues", "outside_collaborators")
delete_url = oc_url + "/" + login
if not dry_run:
response = org._delete(delete_url)
if response.status_code not in [204, ]:
logger.error('ERROR: bad result ({}) from {}'
.format(response.status_code, delete_url))
logger.info('removed {} as outside collaborator via {}'.format(login,
delete_url))
if response.status_code not in [
204,
]:
logger.error(
"ERROR: bad result ({}) from {}".format(
response.status_code, delete_url
)
)
logger.info(f"removed {login} as outside collaborator via {delete_url}")
def parse_args():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("orgs", help='organizations to operate on',
default=['mozilla', ], nargs='*')
parser.add_argument("--login", "-u", help='GitHub user to remove',
required=True)
parser.add_argument("--dry-run", "-n", help='Do not make changes',
action='store_false')
parser.add_argument("--cwd", "-R", help='repo to use', dest='repos',
action='append')
parser.add_argument(
"orgs",
help="organizations to operate on",
default=[
"mozilla",
],
nargs="*",
)
parser.add_argument("--login", "-u", help="GitHub user to remove", required=True)
parser.add_argument(
"--dry-run", "-n", help="Do not make changes", action="store_false"
)
parser.add_argument(
"--cwd", "-R", help="repo to use", dest="repos", action="append"
)
# done with per-run setup
# done with per-run setup
args = parser.parse_args()
return args
@ -83,7 +95,8 @@ def main():
for org in args.orgs:
remove_login_from_org(args.login, org)
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
logging.getLogger('github3').setLevel(logging.WARNING)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
logging.getLogger("github3").setLevel(logging.WARNING)
main()

Просмотреть файл

@ -1,8 +1,6 @@
#!/usr/bin/env python
# PYTHON_ARGCOMPLETE_OK
"""
Report on the non-owner admins of the specified repo
"""
"""Report on the non-owner admins of the specified repo."""
import argparse
import logging
@ -28,9 +26,8 @@ logger = logging.getLogger(__name__)
def unpack_repo(owner_repo, default=None):
"""
unpack a repostitory specification, which might have the owner login in
prepended
"""unpack a repostitory specification, which might have the owner login in
prepended.
>>> unpack_repo('user/repo')
('user', 'repo')
@ -41,7 +38,7 @@ def unpack_repo(owner_repo, default=None):
"""
parts = owner_repo.split("/", 1)
if len(parts) is 2:
if len(parts) == 2:
owner, repo = parts
else:
owner, repo = default, parts[0]
@ -88,9 +85,7 @@ def parse_args(args=None):
@lru_cache(16)
def fetch_owners(login):
"""
get all org owners
"""
"""get all org owners."""
owners = set()
org = gh().organization(login)
for user in org.members(role="admin"):
@ -99,9 +94,7 @@ def fetch_owners(login):
def fetch_admins(owner, repo):
"""
get all repository admins
"""
"""get all repository admins."""
admins = set()
repo = gh().repository(owner, repo)
for collaborator in repo.collaborators():
@ -111,29 +104,27 @@ def fetch_admins(owner, repo):
def process_repo(owner, repo):
"""
Find all repo admins, who are not organization owners
"""
"""Find all repo admins, who are not organization owners."""
owners = fetch_owners(owner)
admins = fetch_admins(owner, repo)
return admins - owners
def email_of(login):
""" return email of login, and link to find in people.m.o
r"""return email of login, and link to find in people.m.o
returned line has email first, then ',' then people.m.o link
returned line has email first, then ',' then people.m.o link
email address can be obtained by piping to `cut -d\, -f 1`
email address can be obtained by piping to `cut -d\, -f 1`
ToDo:
- link to iam, so can map verified github login to email
from there.
ToDo:
- link to iam, so can map verified github login to email
from there.
"""
u = gh().user(login)
pmo_url = "https://people.mozilla.org/s?query={}&who=all".format(login)
pmo_url = f"https://people.mozilla.org/s?query={login}&who=all"
email = u.email if u.email else ""
return "{},{}".format(email, pmo_url)
return f"{email},{pmo_url}"
def main(args=None):
@ -141,7 +132,7 @@ def main(args=None):
for repo in args.repos:
owner_name, repo_name = unpack_repo(repo, default=args.org)
admins = process_repo(owner_name, repo_name)
print("{}/{}:".format(owner_name, repo_name))
print(f"{owner_name}/{repo_name}:")
for login in admins:
output = login if not args.email else email_of(login)
print(output)

Просмотреть файл

@ -1,51 +1,52 @@
#!/usr/bin/env python
from datetime import datetime
import getopt
import urllib
import urllib.request, urllib.parse, urllib.error
import sys
import requests
from functools import reduce
def _parse_github_datetime(datetime_string):
try:
return datetime.strptime(datetime_string, '%Y-%m-%dT%H:%M:%SZ')
return datetime.strptime(datetime_string, "%Y-%m-%dT%H:%M:%SZ")
except TypeError:
pass
def usage():
print "python repo-pr-stats.py <owner> <repo>"
print("python repo-pr-stats.py <owner> <repo>")
def main(argv):
owner = argv[0]
repo = argv[1]
params = {'state': 'closed',
'per_page': 100}
params = {"state": "closed", "per_page": 100}
all_pulls_url = '%s?%s' % (
'https://api.github.com/repos/%s/%s/pulls' % (owner, repo),
urllib.urlencode(params)
all_pulls_url = "{}?{}".format(
f"https://api.github.com/repos/{owner}/{repo}/pulls",
urllib.parse.urlencode(params),
)
resp = requests.get(all_pulls_url)
all_pulls = resp.json()
review_times = []
for pull in all_pulls:
if pull['merged_at'] is not None:
created_at = _parse_github_datetime(pull['created_at'])
merged_at = _parse_github_datetime(pull['merged_at'])
if pull["merged_at"] is not None:
created_at = _parse_github_datetime(pull["created_at"])
merged_at = _parse_github_datetime(pull["merged_at"])
review_time = merged_at - created_at
print "Pull %s review took %s to merge" % (pull['id'], review_time)
print("Pull {} review took {} to merge".format(pull["id"], review_time))
review_times.append(review_time)
print "Total Average Review Time (%s pulls): " % len(review_times)
print reduce(lambda x, y: x + y, review_times) / len(review_times)
print("Total Average Review Time (%s pulls): " % len(review_times))
print(reduce(lambda x, y: x + y, review_times) / len(review_times))
if __name__ == '__main__':
if len(sys.argv) is not 3:
if __name__ == "__main__":
if len(sys.argv) != 3:
usage()
else:
main(sys.argv[1:])

Просмотреть файл

@ -1,14 +1,393 @@
jsonstreams==0.4.1
# we need a version of github3.py with
# https://github.com/sigmavirus24/github3.py/pull/675 landed
# github3.py==1.0.0a4
# git+https://github.com/hwine/github3.py.git@invitations
github3.py==1.1.0
PyYaml==5.1
tinydb==3.2.1
# until up on pypa
git+https://github.com/mozilla/github_selenium#egg=github_selenium
arrow==0.10.0
backports.functools_lru_cache
backoff==1.8.0
argcomplete==1.11.0
appnope==0.1.2; python_version >= "3.3" and sys_platform == "darwin" or platform_system == "Darwin" \
--hash=sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442 \
--hash=sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a
argcomplete==1.12.3 \
--hash=sha256:291f0beca7fd49ce285d2f10e4c1c77e9460cf823eef2de54df0c0fec88b0d81 \
--hash=sha256:2c7dbffd8c045ea534921e63b0be6fe65e88599990d8dc408ac8c542b72a5445
argon2-cffi==20.1.0 \
--hash=sha256:d8029b2d3e4b4cea770e9e5a0104dd8fa185c1724a0f01528ae4826a6d25f97d \
--hash=sha256:6ea92c980586931a816d61e4faf6c192b4abce89aa767ff6581e6ddc985ed003 \
--hash=sha256:05a8ac07c7026542377e38389638a8a1e9b78f1cd8439cd7493b39f08dd75fbf \
--hash=sha256:0bf066bc049332489bb2d75f69216416329d9dc65deee127152caeb16e5ce7d5 \
--hash=sha256:57358570592c46c420300ec94f2ff3b32cbccd10d38bdc12dc6979c4a8484fbc \
--hash=sha256:7d455c802727710e9dfa69b74ccaab04568386ca17b0ad36350b622cd34606fe \
--hash=sha256:b160416adc0f012fb1f12588a5e6954889510f82f698e23ed4f4fa57f12a0647 \
--hash=sha256:9bee3212ba4f560af397b6d7146848c32a800652301843df06b9e8f68f0f7361 \
--hash=sha256:392c3c2ef91d12da510cfb6f9bae52512a4552573a9e27600bdb800e05905d2b \
--hash=sha256:ba7209b608945b889457f949cc04c8e762bed4fe3fec88ae9a6b7765ae82e496 \
--hash=sha256:da7f0445b71db6d3a72462e04f36544b0de871289b0bc8a7cc87c0f5ec7079fa \
--hash=sha256:cc0e028b209a5483b6846053d5fd7165f460a1f14774d79e632e75e7ae64b82b \
--hash=sha256:18dee20e25e4be86680b178b35ccfc5d495ebd5792cd00781548d50880fee5c5 \
--hash=sha256:6678bb047373f52bcff02db8afab0d2a77d83bde61cfecea7c5c62e2335cb203 \
--hash=sha256:77e909cc756ef81d6abb60524d259d959bab384832f0c651ed7dcb6e5ccdbb78 \
--hash=sha256:9dfd5197852530294ecb5795c97a823839258dfd5eb9420233c7cfedec2058f2 \
--hash=sha256:e2db6e85c057c16d0bd3b4d2b04f270a7467c147381e8fd73cbbe5bc719832be \
--hash=sha256:8a84934bd818e14a17943de8099d41160da4a336bcc699bb4c394bbb9b94bd32
arrow==0.12.1 \
--hash=sha256:a558d3b7b6ce7ffc74206a86c147052de23d3d4ef0e17c210dd478c53575c4cd
async-generator==1.10 \
--hash=sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b \
--hash=sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144
attrs==20.3.0 \
--hash=sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6 \
--hash=sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700
backcall==0.2.0; python_version >= "3.3" \
--hash=sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255 \
--hash=sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e
bleach==3.3.0 \
--hash=sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125 \
--hash=sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433
certifi==2020.12.5 \
--hash=sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830 \
--hash=sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c
cffi==1.14.5 \
--hash=sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991 \
--hash=sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1 \
--hash=sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa \
--hash=sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3 \
--hash=sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5 \
--hash=sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482 \
--hash=sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6 \
--hash=sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045 \
--hash=sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa \
--hash=sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406 \
--hash=sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369 \
--hash=sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315 \
--hash=sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892 \
--hash=sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058 \
--hash=sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5 \
--hash=sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132 \
--hash=sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53 \
--hash=sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813 \
--hash=sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73 \
--hash=sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06 \
--hash=sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1 \
--hash=sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49 \
--hash=sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62 \
--hash=sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4 \
--hash=sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053 \
--hash=sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0 \
--hash=sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e \
--hash=sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827 \
--hash=sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e \
--hash=sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396 \
--hash=sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea \
--hash=sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322 \
--hash=sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c \
--hash=sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee \
--hash=sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396 \
--hash=sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d \
--hash=sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c
chardet==4.0.0 \
--hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 \
--hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa
colorama==0.4.4; python_version >= "3.3" and sys_platform == "win32" or sys_platform == "win32" \
--hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 \
--hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b
decorator==5.0.7; python_version >= "3.3" \
--hash=sha256:945d84890bb20cc4a2f4a31fc4311c0c473af65ea318617f13a7257c9a58bc98 \
--hash=sha256:6f201a6c4dac3d187352661f508b9364ec8091217442c9478f1f83c003a0f060
defusedxml==0.7.1 \
--hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 \
--hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69
entrypoints==0.3 \
--hash=sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19 \
--hash=sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451
-e git+https://github.com/mozilla/github_selenium.git@30a6117ebbb5706be279b6ea3124c872a8d09ec5#egg=github-selenium
github3.py==1.0.0a4 \
--hash=sha256:fd7008e18de68769fce6e069aeccf0a165953ace47d66b5fc0466bc59221264c \
--hash=sha256:570e776e2ab1318533b1555ee2f2f784142883b49cb4090582c0307c16cc1666
idna==2.10 \
--hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 \
--hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6
ipykernel==5.5.3 \
--hash=sha256:21abd584543759e49010975a4621603b3cf871b1039cb3879a14094717692614 \
--hash=sha256:a682e4f7affd86d9ce9b699d21bcab6d5ec9fbb2bfcb194f2706973b252bc509
ipython==7.23.0 \
--hash=sha256:3455b020a895710c4366e8d1b326e5ee6aa684607907fc96895e7b8359569f49 \
--hash=sha256:69178f32bf9c6257430b6f592c3ae230c32861a1966d2facec454e09078e232d
ipython-genutils==0.2.0 \
--hash=sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8 \
--hash=sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8
ipywidgets==7.6.3 \
--hash=sha256:e6513cfdaf5878de30f32d57f6dc2474da395a2a2991b94d487406c0ab7f55ca \
--hash=sha256:9f1a43e620530f9e570e4a493677d25f08310118d315b00e25a18f12913c41f0
jedi==0.18.0; python_version >= "3.3" \
--hash=sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93 \
--hash=sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707
jinja2==2.11.3 \
--hash=sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419 \
--hash=sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6
jsonschema==3.2.0 \
--hash=sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163 \
--hash=sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a
jsonstreams==0.4.1 \
--hash=sha256:ed67c2a58a9757214baaf67675db2e504d6c9bc590da86c2e8a5805ad8bc845c \
--hash=sha256:875c03c0a93e1e23a7eaa7a7a89e36290ff29613286bce563dad37fb7061addd
jupyter==1.0.0 \
--hash=sha256:5b290f93b98ffbc21c0c7e749f054b3267782166d72fa5e3ed1ed4eaf34a2b78 \
--hash=sha256:d9dc4b3318f310e34c82951ea5d6683f67bed7def4b259fafbfe4f1beb1d8e5f \
--hash=sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7
jupyter-client==6.2.0 \
--hash=sha256:9715152067e3f7ea3b56f341c9a0f9715c8c7cc316ee0eb13c3c84f5ca0065f5 \
--hash=sha256:e2ab61d79fbf8b56734a4c2499f19830fbd7f6fefb3e87868ef0545cb3c17eb9
jupyter-console==6.4.0 \
--hash=sha256:7799c4ea951e0e96ba8260575423cb323ea5a03fcf5503560fa3e15748869e27 \
--hash=sha256:242248e1685039cd8bff2c2ecb7ce6c1546eb50ee3b08519729e6e881aec19c7
jupyter-core==4.7.1 \
--hash=sha256:8c6c0cac5c1b563622ad49321d5ec47017bd18b94facb381c6973a0486395f8e \
--hash=sha256:79025cb3225efcd36847d0840f3fc672c0abd7afd0de83ba8a1d3837619122b4
jupyterlab-pygments==0.1.2 \
--hash=sha256:abfb880fd1561987efaefcb2d2ac75145d2a5d0139b1876d5be806e32f630008 \
--hash=sha256:cfcda0873626150932f438eccf0f8bf22bfa92345b814890ab360d666b254146
jupyterlab-widgets==1.0.0; python_version >= "3.6" \
--hash=sha256:caeaf3e6103180e654e7d8d2b81b7d645e59e432487c1d35a41d6d3ee56b3fef \
--hash=sha256:5c1a29a84d3069208cb506b10609175b249b6486d6b1cbae8fcde2a11584fb78
markupsafe==1.1.1 \
--hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \
--hash=sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7 \
--hash=sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183 \
--hash=sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b \
--hash=sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e \
--hash=sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f \
--hash=sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1 \
--hash=sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5 \
--hash=sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1 \
--hash=sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735 \
--hash=sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21 \
--hash=sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235 \
--hash=sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b \
--hash=sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f \
--hash=sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905 \
--hash=sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1 \
--hash=sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d \
--hash=sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff \
--hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \
--hash=sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e \
--hash=sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66 \
--hash=sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5 \
--hash=sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d \
--hash=sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e \
--hash=sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6 \
--hash=sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2 \
--hash=sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c \
--hash=sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15 \
--hash=sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2 \
--hash=sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42 \
--hash=sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b \
--hash=sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be \
--hash=sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b
matplotlib-inline==0.1.2; python_version >= "3.3" \
--hash=sha256:f41d5ff73c9f5385775d5c0bc13b424535c8402fe70ea8210f93e11f3683993e \
--hash=sha256:5cf1176f554abb4fa98cb362aa2b55c500147e4bdbb07e3fda359143e1da0811
mistune==0.8.4 \
--hash=sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4 \
--hash=sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e
nbclient==0.5.3 \
--hash=sha256:e79437364a2376892b3f46bedbf9b444e5396cfb1bc366a472c37b48e9551500 \
--hash=sha256:db17271330c68c8c88d46d72349e24c147bb6f34ec82d8481a8f025c4d26589c
nbconvert==6.0.7 \
--hash=sha256:39e9f977920b203baea0be67eea59f7b37a761caa542abe80f5897ce3cf6311d \
--hash=sha256:cbbc13a86dfbd4d1b5dee106539de0795b4db156c894c2c5dc382062bbc29002
nbformat==5.1.3 \
--hash=sha256:eb8447edd7127d043361bc17f2f5a807626bc8e878c7709a1c647abda28a9171 \
--hash=sha256:b516788ad70771c6250977c1374fcca6edebe6126fd2adb5a69aa5c2356fd1c8
nest-asyncio==1.5.1 \
--hash=sha256:76d6e972265063fe92a90b9cc4fb82616e07d586b346ed9d2c89a4187acea39c \
--hash=sha256:afc5a1c515210a23c461932765691ad39e8eba6551c055ac8d5546e69250d0aa
notebook==6.3.0 \
--hash=sha256:cb271af1e8134e3d6fc6d458bdc79c40cbfc84c1eb036a493f216d58f0880e92 \
--hash=sha256:cbc9398d6c81473e9cdb891d2cae9c0d3718fca289dda6d26df5cb660fcadc7d
packaging==20.9 \
--hash=sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a \
--hash=sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5
pandocfilters==1.4.3 \
--hash=sha256:bc63fbb50534b4b1f8ebe1860889289e8af94a23bff7445259592df25a3906eb
parso==0.8.2; python_version >= "3.3" \
--hash=sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22 \
--hash=sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398
pexpect==4.8.0; python_version >= "3.3" and sys_platform != "win32" \
--hash=sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937 \
--hash=sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c
pickleshare==0.7.5; python_version >= "3.3" \
--hash=sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56 \
--hash=sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca
prometheus-client==0.10.1 \
--hash=sha256:030e4f9df5f53db2292eec37c6255957eb76168c6f974e4176c711cf91ed34aa \
--hash=sha256:b6c5a9643e3545bcbfd9451766cbaa5d9c67e7303c7bc32c750b6fa70ecb107d
prompt-toolkit==3.0.18 \
--hash=sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04 \
--hash=sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc
ptyprocess==0.7.0; python_version >= "3.3" and sys_platform != "win32" or os_name != "nt" \
--hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \
--hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220
py==1.10.0 \
--hash=sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a \
--hash=sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3
pycparser==2.20 \
--hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705 \
--hash=sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0
pygments==2.8.1 \
--hash=sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8 \
--hash=sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94
pyparsing==2.4.7 \
--hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b \
--hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1
pyrsistent==0.17.3 \
--hash=sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e
python-dateutil==2.8.1 \
--hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \
--hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a
pywin32==300; sys_platform == "win32" \
--hash=sha256:1c204a81daed2089e55d11eefa4826c05e604d27fe2be40b6bf8db7b6a39da63 \
--hash=sha256:350c5644775736351b77ba68da09a39c760d75d2467ecec37bd3c36a94fbed64 \
--hash=sha256:a3b4c48c852d4107e8a8ec980b76c94ce596ea66d60f7a697582ea9dce7e0db7 \
--hash=sha256:27a30b887afbf05a9cbb05e3ffd43104a9b71ce292f64a635389dbad0ed1cd85 \
--hash=sha256:d7e8c7efc221f10d6400c19c32a031add1c4a58733298c09216f57b4fde110dc \
--hash=sha256:8151e4d7a19262d6694162d6da85d99a16f8b908949797fd99c83a0bfaf5807d \
--hash=sha256:fbb3b1b0fbd0b4fc2a3d1d81fe0783e30062c1abed1d17c32b7879d55858cfae \
--hash=sha256:60a8fa361091b2eea27f15718f8eb7f9297e8d51b54dbc4f55f3d238093d5190 \
--hash=sha256:638b68eea5cfc8def537e43e9554747f8dee786b090e47ead94bfdafdb0f2f50 \
--hash=sha256:b1609ce9bd5c411b81f941b246d683d6508992093203d4eb7f278f4ed1085c3f
pywinpty==1.0.1; os_name == "nt" \
--hash=sha256:739094e8d0d685a64c92ff91424cf43da9296110349036161ab294268e444d05 \
--hash=sha256:5447b8c158e5807237f80ea4e14262f0c05ff7c4d39f1c4b697ea6e8920786b2 \
--hash=sha256:aa3e4178503ff6be3e8a1d9ae4ce77de9058308562dbf26b505a51583be9f02d \
--hash=sha256:58e23d59891e624d478ec7bcc42ced0ecfbf0a4e7cb0217de714f785f71c2461 \
--hash=sha256:b3512d4a964a0abae1b77b6908917c62ea0ad7d8178696e4e973877fe9e820f9
pyyaml==5.1.2 \
--hash=sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8 \
--hash=sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8 \
--hash=sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9 \
--hash=sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696 \
--hash=sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41 \
--hash=sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73 \
--hash=sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299 \
--hash=sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b \
--hash=sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae \
--hash=sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34 \
--hash=sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9 \
--hash=sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681 \
--hash=sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4
pyzmq==22.0.3 \
--hash=sha256:c0cde362075ee8f3d2b0353b283e203c2200243b5a15d5c5c03b78112a17e7d4 \
--hash=sha256:ff1ea14075bbddd6f29bf6beb8a46d0db779bcec6b9820909584081ec119f8fd \
--hash=sha256:26380487eae4034d6c2a3fb8d0f2dff6dd0d9dd711894e8d25aa2d1938950a33 \
--hash=sha256:3e29f9cf85a40d521d048b55c63f59d6c772ac1c4bf51cdfc23b62a62e377c33 \
--hash=sha256:4f34a173f813b38b83f058e267e30465ed64b22cd0cf6bad21148d3fa718f9bb \
--hash=sha256:30df70f81fe210506aa354d7fd486a39b87d9f7f24c3d3f4f698ec5d96b8c084 \
--hash=sha256:7026f0353977431fc884abd4ac28268894bd1a780ba84bb266d470b0ec26d2ed \
--hash=sha256:6d4163704201fff0f3ab0cd5d7a0ea1514ecfffd3926d62ec7e740a04d2012c7 \
--hash=sha256:763c175294d861869f18eb42901d500eda7d3fa4565f160b3b2fd2678ea0ebab \
--hash=sha256:61e4bb6cd60caf1abcd796c3f48395e22c5b486eeca6f3a8797975c57d94b03e \
--hash=sha256:b25e5d339550a850f7e919fe8cb4c8eabe4c917613db48dab3df19bfb9a28969 \
--hash=sha256:3ef50d74469b03725d781a2a03c57537d86847ccde587130fe35caafea8f75c6 \
--hash=sha256:60e63577b85055e4cc43892fecd877b86695ee3ef12d5d10a3c5d6e77a7cc1a3 \
--hash=sha256:f5831eff6b125992ec65d973f5151c48003b6754030094723ac4c6e80a97c8c4 \
--hash=sha256:9221783dacb419604d5345d0e097bddef4459a9a95322de6c306bf1d9896559f \
--hash=sha256:b62ea18c0458a65ccd5be90f276f7a5a3f26a6dea0066d948ce2fa896051420f \
--hash=sha256:81e7df0da456206201e226491aa1fc449da85328bf33bbeec2c03bb3a9f18324 \
--hash=sha256:f52070871a0fd90a99130babf21f8af192304ec1e995bec2a9533efc21ea4452 \
--hash=sha256:c5e29fe4678f97ce429f076a2a049a3d0b2660ada8f2c621e5dc9939426056dd \
--hash=sha256:d18ddc6741b51f3985978f2fda57ddcdae359662d7a6b395bc8ff2292fca14bd \
--hash=sha256:4231943514812dfb74f44eadcf85e8dd8cf302b4d0bce450ce1357cac88dbfdc \
--hash=sha256:23a74de4b43c05c3044aeba0d1f3970def8f916151a712a3ac1e5cd9c0bc2902 \
--hash=sha256:532af3e6dddea62d9c49062ece5add998c9823c2419da943cf95589f56737de0 \
--hash=sha256:33acd2b9790818b9d00526135acf12790649d8d34b2b04d64558b469c9d86820 \
--hash=sha256:a558c5bc89d56d7253187dccc4e81b5bb0eac5ae9511eb4951910a1245d04622 \
--hash=sha256:581787c62eaa0e0db6c5413cedc393ebbadac6ddfd22e1cf9a60da23c4f1a4b2 \
--hash=sha256:38e3dca75d81bec4f2defa14b0a65b74545812bb519a8e89c8df96bbf4639356 \
--hash=sha256:2f971431aaebe0a8b54ac018e041c2f0b949a43745444e4dadcc80d0f0ef8457 \
--hash=sha256:da7d4d4c778c86b60949d17531e60c54ed3726878de8a7f8a6d6e7f8cc8c3205 \
--hash=sha256:13465c1ff969cab328bc92f7015ce3843f6e35f8871ad79d236e4fbc85dbe4cb \
--hash=sha256:279cc9b51db48bec2db146f38e336049ac5a59e5f12fb3a8ad864e238c1c62e3 \
--hash=sha256:f7f63ce127980d40f3e6a5fdb87abf17ce1a7c2bd8bf2c7560e1bbce8ab1f92d
qtconsole==5.0.3 \
--hash=sha256:4a38053993ca2da058f76f8d75b3d8906efbf9183de516f92f222ac8e37d9614 \
--hash=sha256:c091a35607d2a2432e004c4a112d241ce908086570cf68594176dd52ccaa212d
qtpy==1.9.0 \
--hash=sha256:fa0b8363b363e89b2a6f49eddc162a04c0699ae95e109a6be3bb145a913190ea \
--hash=sha256:2db72c44b55d0fe1407be8fba35c838ad0d6d3bb81f23007886dc1fc0f459c8d
requests==2.25.1 \
--hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e \
--hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804
selenium==3.141.0 \
--hash=sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c \
--hash=sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d
send2trash==1.5.0 \
--hash=sha256:f1691922577b6fa12821234aeb57599d887c4900b9ca537948d2dac34aea888b \
--hash=sha256:60001cc07d707fe247c94f74ca6ac0d3255aabcb930529690897ca2a39db28b2
six==1.15.0 \
--hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced \
--hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259
terminado==0.9.4 \
--hash=sha256:daed77f9fad7b32558fa84b226a76f45a02242c20813502f36c4e1ade6d8f1ad \
--hash=sha256:9a7dbcfbc2778830eeb70261bf7aa9d98a3eac8631a3afe3febeb57c12f798be
testpath==0.4.4 \
--hash=sha256:bfcf9411ef4bf3db7579063e0546938b1edda3d69f4e1fb8756991f5951f85d4 \
--hash=sha256:60e0a3261c149755f4399a1fff7d37523179a70fdc3abdf78de9fc2604aeec7e
tinydb==3.2.1 \
--hash=sha256:7fc5bfc2439a0b379bd60638b517b52bcbf70220195b3f3245663cb8ad9dbcf0
tornado==6.1 \
--hash=sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32 \
--hash=sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c \
--hash=sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05 \
--hash=sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910 \
--hash=sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b \
--hash=sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675 \
--hash=sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5 \
--hash=sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68 \
--hash=sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb \
--hash=sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c \
--hash=sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921 \
--hash=sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558 \
--hash=sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c \
--hash=sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085 \
--hash=sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575 \
--hash=sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795 \
--hash=sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f \
--hash=sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102 \
--hash=sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4 \
--hash=sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd \
--hash=sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01 \
--hash=sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d \
--hash=sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df \
--hash=sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37 \
--hash=sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95 \
--hash=sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a \
--hash=sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5 \
--hash=sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288 \
--hash=sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f \
--hash=sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6 \
--hash=sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326 \
--hash=sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c \
--hash=sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5 \
--hash=sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe \
--hash=sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea \
--hash=sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2 \
--hash=sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0 \
--hash=sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd \
--hash=sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c \
--hash=sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4 \
--hash=sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791
traitlets==5.0.5 \
--hash=sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426 \
--hash=sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396
uritemplate==3.0.1 \
--hash=sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f \
--hash=sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae
uritemplate.py==3.0.2 \
--hash=sha256:a0c459569e80678c473175666e0d1b3af5bc9a13f84463ec74f808f3dd12ca47 \
--hash=sha256:e0cdeb0f55ec18e1580974e8017cd188549aacc2aba664ae756adb390b9d45b4
urllib3==1.26.4 \
--hash=sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df \
--hash=sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937
wcwidth==0.2.5 \
--hash=sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784 \
--hash=sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83
webencodings==0.5.1 \
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
--hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
widgetsnbextension==3.5.1 \
--hash=sha256:bd314f8ceb488571a5ffea6cc5b9fc6cba0adaf88a9d2386b93a489751938bcd \
--hash=sha256:079f87d87270bce047512400efd70238820751a11d2d8cb137a5a5bdbaf255c7

Просмотреть файл

@ -1 +0,0 @@
python-2.7

Просмотреть файл

@ -1,9 +1,8 @@
#!/usr/bin/env python
"""
Update the membership of a team based on GitHub attributes
"""Update the membership of a team based on GitHub attributes.
e.g. make a team of 'owners' or 'members'. The team must already exist -
see --help output for names
e.g. make a team of 'owners' or 'members'. The team must already exist -
see --help output for names
"""
import argparse
import logging
@ -13,7 +12,7 @@ from client import get_github3_client
import github3
TEAM = 'admin-all-org-'
TEAM = "admin-all-org-"
VERBOSE = False
logger = logging.getLogger(__name__)
@ -32,76 +31,93 @@ def update_team_membership(org, new_member_list, team_name=None, do_update=False
# team must already exist in the org
team = [x for x in org.teams() if x.name == team_name][0]
# get set of current members
current = set([x.login for x in team.members()])
current = {x.login for x in team.members()}
# get set of new members
new = set([x.login for x in new_member_list])
new = {x.login for x in new_member_list}
to_remove = current - new
to_add = new - current
no_change = new & current
update_success = True
print "%5d alumni" % len(to_remove)
print("%5d alumni" % len(to_remove))
for login in to_remove:
if do_update and not team.remove_member(login):
logger.warn("Failed to remove a member"
" - you need 'admin:org' permissions")
logger.warn(
"Failed to remove a member" " - you need 'admin:org' permissions"
)
update_success = False
break
if VERBOSE:
print(" {} has departed".format(login))
print "%5d new" % len(to_add)
print(f" {login} has departed")
print("%5d new" % len(to_add))
for login in to_add:
if VERBOSE:
print(" {} is new".format(login))
print(f" {login} is new")
try:
if do_update and not team.add_member(login):
logger.warn("Failed to add a member"
" - you need 'admin:org' permissions")
logger.warn(
"Failed to add a member" " - you need 'admin:org' permissions"
)
update_success = False
break
except github3.exceptions.ForbiddenError:
# this occurs occasionally, don't stop work
logger.warn("Failed to add member '{}'".format(login))
logger.warn(f"Failed to add member '{login}'")
update_success = False
print "%5d no change" % len(no_change)
print("%5d no change" % len(no_change))
# if we're running in the ipython notebook, the log message isn't
# displayed. Output something useful
if not update_success:
print "Updates were not all made to team '%s' in '%s'." % (team_name, org.name)
print "Make sure your API token has 'admin:org' permissions for that organization."
print(f"Updates were not all made to team '{team_name}' in '{org.name}'.")
print(
"Make sure your API token has 'admin:org' permissions for that organization."
)
def check_users(gh, org_name, admins_only=True, update_team=False):
org = gh.organization(org_name)
if not org:
print('No such org found!')
print("No such org found!")
sys.exit(1)
role = 'admin' if admins_only else 'all'
user_type = 'owners' if admins_only else 'members'
role = "admin" if admins_only else "all"
user_type = "owners" if admins_only else "members"
members = list(org.members(role=role))
if members:
print('There are %d %s for org %s:' %
(len(members), user_type, org_name))
print("There are %d %s for org %s:" % (len(members), user_type, org_name))
else:
print("Error: no %s found for %s" % (user_type, org_name))
print(f"Error: no {user_type} found for {org_name}")
if update_team or VERBOSE:
update_team_membership(org, members, team_name(user_type), update_team)
def parse_args():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--owners', action='store_true',
help='Report only for org owners (default all members)')
parser.add_argument("orgs", nargs='*', default=['mozilla', ],
help='github organizations to check (defaults to mozilla)')
parser.add_argument("--team", default=TEAM,
help='update membership of team "%s{owners,members}"' % TEAM)
parser.add_argument("--update-team", action='store_true',
help='apply changes to GitHub')
parser.add_argument("--verbose", action='store_true',
help='print logins for all changes')
parser.add_argument(
"--owners",
action="store_true",
help="Report only for org owners (default all members)",
)
parser.add_argument(
"orgs",
nargs="*",
default=[
"mozilla",
],
help="github organizations to check (defaults to mozilla)",
)
parser.add_argument(
"--team",
default=TEAM,
help='update membership of team "%s{owners,members}"' % TEAM,
)
parser.add_argument(
"--update-team", action="store_true", help="apply changes to GitHub"
)
parser.add_argument(
"--verbose", action="store_true", help="print logins for all changes"
)
return parser.parse_args()
@ -117,6 +133,6 @@ def main():
check_users(gh, org, args.owners, args.update_team)
if __name__ == '__main__':
logging.basicConfig(level=logging.WARN, format='%(asctime)s %(message)s')
if __name__ == "__main__":
logging.basicConfig(level=logging.WARN, format="%(asctime)s %(message)s")
main()

0
tests/__init__.py Normal file
Просмотреть файл

Просмотреть файл

@ -0,0 +1,10 @@
import get_org_info
import pytest
def test_encoding_succeeds(capsys):
"""Base64 encoding broke on move to py3 - make sure it stays fixed"""
get_org_info.main()
captured = capsys.readouterr()
token = "MDEyOk9yZ2FuaXphdGlvbjEzMTUyNA==" # nosec
assert token in captured.out # nosec