Родитель
94af68c3a7
Коммит
62a28ac8da
|
@ -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.
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -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"
|
||||
|
|
75
Makefile
75
Makefile
|
@ -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
|
||||
|
|
20
README.md
20
README.md
|
@ -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)
|
85
auditlog.py
85
auditlog.py
|
@ -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
|
||||
|
|
30
client.py
30
client.py
|
@ -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
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:
|
||||
|
|
75
old_repos.py
75
old_repos.py
|
@ -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()
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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"
|
|
@ -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()
|
||||
|
|
41
repo-admins
41
repo-admins
|
@ -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:])
|
||||
|
|
407
requirements.txt
407
requirements.txt
|
@ -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,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
|
Загрузка…
Ссылка в новой задаче