Port samples from private repository
This commit is contained in:
Родитель
039789d80c
Коммит
a1b676a3ee
|
@ -1,3 +1,9 @@
|
|||
# Project
|
||||
vsts-runner-config.json
|
||||
|
||||
# Editors
|
||||
.vscode/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
@ -86,6 +92,7 @@ celerybeat-schedule
|
|||
.venv
|
||||
venv/
|
||||
ENV/
|
||||
env*/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"\"\"\"\n",
|
||||
"Visual Studio Team Services\n",
|
||||
"Python API samples\n",
|
||||
"\n",
|
||||
"Click \"Run\" in the Jupyter menu to get started.\n",
|
||||
"\n",
|
||||
" vSTs\n",
|
||||
" vSTSVSTSv\n",
|
||||
" vSTSVSTSVST\n",
|
||||
" VSTS vSTSVSTSVSTSV\n",
|
||||
" VSTSVS vSTSVSTSV STSVS\n",
|
||||
" VSTSVSTSvsTSVSTSVS TSVST\n",
|
||||
" VS tSVSTSVSTSv STSVS\n",
|
||||
" VS tSVSTSVST SVSTS\n",
|
||||
" VS tSVSTSVSTSVSts VSTSV\n",
|
||||
" VSTSVST SVSTSVSTs VSTSV\n",
|
||||
" VSTSv STSVSTSVSTSVS\n",
|
||||
" VSTSVSTSVST\n",
|
||||
" VSTSVSTs\n",
|
||||
" VSTs (TM)\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
"import sys\n",
|
||||
"# tell python to look in .\\src for loading modules\n",
|
||||
"sys.path.insert(1, 'src')\n",
|
||||
"\n",
|
||||
"from IPython.display import display\n",
|
||||
"import ipywidgets as widgets\n",
|
||||
"\n",
|
||||
"import runner\n",
|
||||
"import runner_lib\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def build_areas_and_resources():\n",
|
||||
" value = {\n",
|
||||
" 'all': ['all',],\n",
|
||||
" }\n",
|
||||
" \n",
|
||||
" for area in runner_lib.discovered_samples.keys():\n",
|
||||
" value[area] = ['all',]\n",
|
||||
" value[area].extend(runner_lib.discovered_samples[area].keys())\n",
|
||||
" \n",
|
||||
" return value\n",
|
||||
"\n",
|
||||
"areas_and_resources = build_areas_and_resources()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# build and display widgets\n",
|
||||
"url_widget = widgets.Text(\n",
|
||||
" value='',\n",
|
||||
" placeholder='https://fabrikam.visualstudio.com',\n",
|
||||
" description='Account URL'\n",
|
||||
")\n",
|
||||
"pat_widget = widgets.Text(\n",
|
||||
" value='',\n",
|
||||
" placeholder='VSTS PAT',\n",
|
||||
" description='PAT'\n",
|
||||
")\n",
|
||||
"area_widget = widgets.ToggleButtons(\n",
|
||||
" options=areas_and_resources.keys(),\n",
|
||||
" description='Area'\n",
|
||||
")\n",
|
||||
"resource_widget = widgets.ToggleButtons(\n",
|
||||
" options=areas_and_resources[area_widget.value],\n",
|
||||
" description='Resource'\n",
|
||||
")\n",
|
||||
"run_widget = widgets.Button(\n",
|
||||
" description='Run samples',\n",
|
||||
" button_style='success',\n",
|
||||
" icon='play'\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"def on_area_change(change):\n",
|
||||
" resource_widget.options = areas_and_resources[change['new']]\n",
|
||||
"\n",
|
||||
"area_widget.observe(on_area_change, names='value')\n",
|
||||
"\n",
|
||||
"def on_run_click(b):\n",
|
||||
" if not url_widget.value or not pat_widget.value:\n",
|
||||
" print('You must specify a URL and PAT to run these samples.')\n",
|
||||
" return\n",
|
||||
" \n",
|
||||
" print('running samples...')\n",
|
||||
" print('-------------------------')\n",
|
||||
" print()\n",
|
||||
" runner.main(url=url_widget.value,\n",
|
||||
" auth_token=pat_widget.value,\n",
|
||||
" area=area_widget.value,\n",
|
||||
" resource=resource_widget.value\n",
|
||||
" )\n",
|
||||
" print()\n",
|
||||
" print('-------------------------')\n",
|
||||
" print('done!')\n",
|
||||
"\n",
|
||||
"run_widget.on_click(on_run_click)\n",
|
||||
"\n",
|
||||
"display(url_widget)\n",
|
||||
"display(pat_widget)\n",
|
||||
"display(area_widget)\n",
|
||||
"display(resource_widget)\n",
|
||||
"display(run_widget)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.6.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
123
README.md
123
README.md
|
@ -1,14 +1,119 @@
|
|||
# Visual Studio Team Services Samples for Python
|
||||
|
||||
# Contributing
|
||||
This repository contains Python samples that show how to integrate with Visual Studio Team Services (VSTS) and Team Foundation Server (TFS) using [the VSTS Python API](https://github/Microsoft/vsts-python-api/).
|
||||
|
||||
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
||||
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
|
||||
the rights to use your contribution. For details, visit https://cla.microsoft.com.
|
||||
## Explore
|
||||
|
||||
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
|
||||
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
|
||||
provided by the bot. You will only need to do this once across all repos using our CLA.
|
||||
Samples are organized by "area" (service) and "resource" within the `samples` package.
|
||||
Each sample module shows various ways for interacting with VSTS and TFS.
|
||||
Resources may have multiple samples, since there are often multiple ways to query for a given resource.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone this repository and `cd` into it
|
||||
|
||||
2. Create a virtual environment (`python3 -m venv env && . env/bin/activate && pip install -r requirements.txt`)
|
||||
|
||||
Now you can run `runner.py` with no arguments to see available options.
|
||||
|
||||
## Run the samples - command line
|
||||
|
||||
1. Get a [personal access token](https://docs.microsoft.com/en-us/vsts/accounts/use-personal-access-tokens-to-authenticate).
|
||||
|
||||
2. Store the PAT and base account URL you'll be running samples against:
|
||||
* `runner.py config url --set-to https://fabrikam.visualstudio.com`
|
||||
* `runner.py config pat --set-to ABC123`
|
||||
* If you don't want your PAT persisted to a file, you can put it in an environment variable called `VSTS_PAT` instead
|
||||
|
||||
3. Run `runner.py run {area} {resource}` with the 2 required arguments:
|
||||
* `{area}`: API area (currently core, git, and work_item_tracking) to run the client samples for. Use `all` to include all areas.
|
||||
* `{resource}`: API resource to run the client samples for. Use `all` to include all resources.
|
||||
* You can optionally pass `--url {url}` to override your configured URL
|
||||
|
||||
> **IMPORTANT**: some samples are destructive. It is recommended that you first run the samples against a test account.
|
||||
|
||||
### Examples
|
||||
|
||||
#### Run all samples
|
||||
|
||||
```
|
||||
python runner.py run all all
|
||||
```
|
||||
|
||||
#### Run all work item tracking samples
|
||||
|
||||
```
|
||||
python runner.py run work_item_tracking all
|
||||
```
|
||||
|
||||
#### Run all Git pull request samples
|
||||
|
||||
```
|
||||
python runner.py run git pullrequests
|
||||
```
|
||||
|
||||
#### Run all Git samples against a different URL than the one configured; in this case, a TFS on-premises collection
|
||||
|
||||
```
|
||||
python runner.py run git all --url https://mytfs:8080/tfs/testcollection
|
||||
```
|
||||
|
||||
### Save request and response data to a JSON file
|
||||
|
||||
To persist the HTTP request/response as JSON for each client sample method that is run, set the `--output-path {value}` argument. For example:
|
||||
|
||||
```
|
||||
python runner.py run all all --output-path ~/temp/http-output
|
||||
```
|
||||
|
||||
This creates a folder for each area, a folder for each resource under the area folder, and a file for each client sample method that was run. The name of the JSON file is determined by the name of the client sample method. For example:
|
||||
|
||||
```
|
||||
|-- temp
|
||||
|-- http-output
|
||||
|-- git
|
||||
|-- refs
|
||||
|-- get_refs.json
|
||||
|-- ...
|
||||
|-- repositories
|
||||
|-- get_repositories.json
|
||||
|-- ...
|
||||
```
|
||||
|
||||
Note: certain HTTP headers like `Authorization` are removed for security/privacy purposes.
|
||||
|
||||
## See what samples are available
|
||||
|
||||
You can run `runner.py list` to see what sample areas and resources are available.
|
||||
|
||||
## Run the samples - Jupyter notebook
|
||||
|
||||
We also provide a Jupyter notebook for running the samples.
|
||||
You'll get a web browser where you can enter URL, authentication token, and choose which samples you wish to run.
|
||||
|
||||
1. Clone this repository and `cd` into it
|
||||
|
||||
2. Create a virtual environment (`python3 -m venv env && . env/bin/activate && pip install -r requirements.jupyter.txt`)
|
||||
|
||||
3. Get a personal access token.
|
||||
|
||||
4. Run `jupyter notebook`. In the resulting web browser, click **API Samples.ipynb**.
|
||||
|
||||
5. Click **Run** in the top cell. Scroll down and you'll see a form where you can enter your account or collection URL, PAT, and choose which samples to run.
|
||||
|
||||
> **IMPORTANT**: some samples are destructive. It is recommended that you first run the samples against a test account.
|
||||
|
||||
## Contribute
|
||||
|
||||
This project welcomes contributions and suggestions.
|
||||
Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution.
|
||||
For details, visit https://cla.microsoft.com.
|
||||
|
||||
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment).
|
||||
Simply follow the instructions provided by the bot.
|
||||
You will only need to do this once across all repos using our CLA.
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
|
||||
See detailed instructions on how to [contribute a sample](./contribute.md).
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
# Contribute to the samples
|
||||
|
||||
## Organization and style
|
||||
|
||||
1. Samples for an API area should live together under the `samples` package. Each module is an area.
|
||||
```
|
||||
|-- src
|
||||
|-- samples
|
||||
|-- git.py
|
||||
```
|
||||
|
||||
2. Within a module, create a method for each sample. Samples are not object-oriented.
|
||||
* The name of the method should represent what the sample is demonstrating, e.g. `get_{resource}`:
|
||||
```python
|
||||
def get_repos(...):
|
||||
```
|
||||
* The method must accept a `context` parameter, which will contain information passed in from the sample runner infrastructure:
|
||||
```python
|
||||
def get_repos(context):
|
||||
```
|
||||
* The method must decorated with `@resource('resource_name')` to be detected:
|
||||
```python
|
||||
from samples import resource
|
||||
|
||||
@resource('repositories')
|
||||
def get_repos(context):
|
||||
```
|
||||
* Results should be logged using the `utils.emit` function:
|
||||
```python
|
||||
from samples import resource
|
||||
from utils import emit, find_any_project
|
||||
|
||||
@resource('repositories')
|
||||
def get_repos(context):
|
||||
project = find_any_project(context)
|
||||
|
||||
git_client = context.connection.get_client("vsts.git.git_client.GitClient")
|
||||
|
||||
repos = git_client.get_repositories(project.id)
|
||||
for repo in repos:
|
||||
emit(repo)
|
||||
|
||||
return repos
|
||||
```
|
||||
|
||||
|
||||
3. Coding and style
|
||||
* Samples should show catching exceptions for APIs where exceptions are common
|
||||
* Use line breaks and empty lines to help delineate important sections or lines that need to stand out
|
||||
* Use the same "dummy" data across all samples so it's easier to correlate similar concepts
|
||||
* Be as Pythonic and PEP8-y as you can be without violating the above principles. `flake8` is your friend, and should run without anything triggering. (Non-standard default: 120-character lines are allowed.)
|
||||
|
||||
4. All samples **MUST** be runnable on their own without any input
|
||||
|
||||
5. All samples **SHOULD** clean up after themselves.
|
||||
Have a sample method create a resource (to demonstrate creation).
|
||||
Have a later sample method delete the previously created resource.
|
||||
In between the creation and deletion, you can show updating the resource (if applicable)
|
|
@ -0,0 +1,4 @@
|
|||
# if you want to load the Jupyter notebook, use this requirements file
|
||||
ipywidgets==7.0.5
|
||||
jupyter==1.0.0
|
||||
-r requirements.txt
|
|
@ -0,0 +1,2 @@
|
|||
# if you just want the command-line samples experience, use this requirements file
|
||||
vsts==0.1.0b2
|
|
@ -0,0 +1,3 @@
|
|||
[flake8]
|
||||
exclude = __pycache__
|
||||
max-line-length = 120
|
|
@ -0,0 +1,58 @@
|
|||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
from utils import emit
|
||||
|
||||
|
||||
DEFAULT_CONFIG_FILE_NAME = "vsts-runner-config.json"
|
||||
CONFIG_KEYS = [
|
||||
'url',
|
||||
'pat',
|
||||
]
|
||||
|
||||
|
||||
class Config():
|
||||
def __init__(self, filename=None):
|
||||
if not filename:
|
||||
runner_path = (pathlib.Path(os.getcwd()) / pathlib.Path(sys.argv[0])).resolve()
|
||||
filename = runner_path.parents[0] / pathlib.Path(DEFAULT_CONFIG_FILE_NAME)
|
||||
|
||||
self._filename = filename
|
||||
|
||||
try:
|
||||
with open(filename) as config_fp:
|
||||
self._config = json.load(config_fp)
|
||||
except FileNotFoundError:
|
||||
self._config = {}
|
||||
except json.JSONDecodeError:
|
||||
emit("possible bug: config file exists but isn't parseable")
|
||||
self._config = {}
|
||||
|
||||
def __getitem__(self, name):
|
||||
self._check_if_name_valid(name)
|
||||
return self._config.get(name, None)
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
self._check_if_name_valid(name)
|
||||
self._config[name] = value
|
||||
|
||||
def __delitem__(self, name):
|
||||
self._check_if_name_valid(name)
|
||||
self._config.pop(name, None)
|
||||
|
||||
def __len__(self):
|
||||
return len(CONFIG_KEYS)
|
||||
|
||||
def __iter__(self):
|
||||
for key in CONFIG_KEYS:
|
||||
yield key
|
||||
|
||||
def save(self):
|
||||
with open(self._filename, 'w') as config_fp:
|
||||
json.dump(self._config, config_fp, sort_keys=True, indent=4)
|
||||
|
||||
def _check_if_name_valid(self, name):
|
||||
if name not in CONFIG_KEYS:
|
||||
raise KeyError("{0} is not a valid config key".format(name))
|
|
@ -0,0 +1,8 @@
|
|||
"""
|
||||
Exception classes used in the sample runner.
|
||||
"""
|
||||
|
||||
|
||||
class AccountStateError(Exception):
|
||||
"For when an account doesn't have the right preconditions to support a sample."
|
||||
pass
|
|
@ -0,0 +1,19 @@
|
|||
import logging
|
||||
import warnings
|
||||
|
||||
from http_logging import requests_hook
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def add_request_hook(client):
|
||||
"This is a bit of a hack until we have a supported way to install the hook."
|
||||
warnings.warn("hacking in the request hook", DeprecationWarning)
|
||||
|
||||
if requests_hook in client.config.hooks:
|
||||
logger.debug("hook already installed; skipped")
|
||||
return
|
||||
|
||||
logger.debug("installing hook")
|
||||
client.config.hooks.append(requests_hook)
|
|
@ -0,0 +1,106 @@
|
|||
"""
|
||||
HTTP logger hook (for dumping the request/response cycle).
|
||||
"""
|
||||
from contextlib import contextmanager
|
||||
import json
|
||||
|
||||
|
||||
###
|
||||
# Logging state management
|
||||
###
|
||||
|
||||
_enabled_stack = [False]
|
||||
target = None
|
||||
|
||||
|
||||
def push_state(enabled):
|
||||
_enabled_stack.append(enabled)
|
||||
|
||||
|
||||
def pop_state():
|
||||
if len(_enabled_stack) == 1:
|
||||
# never pop the last state
|
||||
return bool(_enabled_stack[0])
|
||||
elif len(_enabled_stack) == 0:
|
||||
# something's gone terribly wrong
|
||||
raise RuntimeError("_enabled_stack should never be empty")
|
||||
else:
|
||||
return bool(_enabled_stack.pop())
|
||||
|
||||
|
||||
def logging_enabled():
|
||||
return bool(_enabled_stack[-1])
|
||||
|
||||
|
||||
@contextmanager
|
||||
def temporarily_disabled():
|
||||
"""Temporarily disable logging if it's enabled.
|
||||
|
||||
with http_logging.temporarily_disabled():
|
||||
my_client.do_thing()
|
||||
|
||||
"""
|
||||
push_state(False)
|
||||
yield
|
||||
pop_state()
|
||||
|
||||
|
||||
###
|
||||
# Actual HTTP logging and hook
|
||||
###
|
||||
|
||||
def _trim_headers(headers):
|
||||
sensitive_headers = [
|
||||
"X-VSS-PerfData",
|
||||
"X-TFS-Session",
|
||||
"X-VSS-E2EID",
|
||||
"X-VSS-Agent",
|
||||
"Authorization",
|
||||
"X-TFS-ProcessId",
|
||||
"X-VSS-UserData",
|
||||
"ActivityId",
|
||||
"P3P",
|
||||
"X-Powered-By",
|
||||
"Cookie",
|
||||
]
|
||||
|
||||
cleaned_headers = headers.copy()
|
||||
|
||||
for sensitive_header in sensitive_headers:
|
||||
try:
|
||||
del cleaned_headers[sensitive_header]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return dict(cleaned_headers)
|
||||
|
||||
|
||||
def log_request(response, file):
|
||||
try:
|
||||
content = response.json()
|
||||
except ValueError:
|
||||
content = response.text
|
||||
|
||||
data = {
|
||||
'request': {
|
||||
'url': response.request.url,
|
||||
'headers': _trim_headers(response.request.headers),
|
||||
'body': str(response.request.body),
|
||||
'method': response.request.method,
|
||||
},
|
||||
'response': {
|
||||
'headers': _trim_headers(response.headers),
|
||||
'body': content,
|
||||
'status': response.status_code,
|
||||
'url': response.url,
|
||||
},
|
||||
}
|
||||
|
||||
json.dump(data, file, indent=4)
|
||||
|
||||
|
||||
def requests_hook(response, *args, **kwargs):
|
||||
global target
|
||||
|
||||
if logging_enabled() and target is not None:
|
||||
log_request(response, target)
|
|
@ -0,0 +1,189 @@
|
|||
"""
|
||||
VSTS Python API sample runner.
|
||||
"""
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
# logging.basicConfig(level=logging.INFO)
|
||||
|
||||
from vsts.credentials import BasicAuthentication
|
||||
from vsts.vss_connection import VssConnection
|
||||
|
||||
from config import Config
|
||||
import http_logging
|
||||
import hacks
|
||||
import runner_lib
|
||||
from utils import emit
|
||||
|
||||
__VERSION__ = "1.0.0"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main(url, area, resource, auth_token, output_path=None):
|
||||
|
||||
context = SimpleNamespace()
|
||||
context.runner_cache = SimpleNamespace()
|
||||
|
||||
# setup the connection
|
||||
context.connection = VssConnection(
|
||||
base_url=url,
|
||||
creds=BasicAuthentication('PAT', auth_token),
|
||||
user_agent='vsts-python-samples/' + __VERSION__)
|
||||
|
||||
# if the user asked for logging:
|
||||
# - add a hook for logging the http request
|
||||
# - create the root directory
|
||||
if output_path:
|
||||
# monkey-patch the get_client method to attach our hook
|
||||
_get_client = context.connection.get_client
|
||||
|
||||
def get_client_with_hook(*args, **kwargs):
|
||||
logger.debug("get_client_with_hook")
|
||||
client = _get_client(*args, **kwargs)
|
||||
hacks.add_request_hook(client)
|
||||
return client
|
||||
context.connection.get_client = get_client_with_hook
|
||||
|
||||
root_log_dir = pathlib.Path(output_path)
|
||||
if not root_log_dir.exists():
|
||||
root_log_dir.mkdir(parents=True, exist_ok=True)
|
||||
http_logging.push_state(True)
|
||||
else:
|
||||
root_log_dir = None
|
||||
|
||||
# runner_lib.discovered_samples will contain a key for each area loaded,
|
||||
# and each key will have the resources and sample functions discovered
|
||||
if area == 'all':
|
||||
areas = runner_lib.discovered_samples.keys()
|
||||
else:
|
||||
if area not in runner_lib.discovered_samples.keys():
|
||||
raise ValueError("area '%s' doesn't exist" % (area,))
|
||||
areas = [area]
|
||||
|
||||
for area in areas:
|
||||
|
||||
area_logging_path = runner_lib.enter_area(area, root_log_dir)
|
||||
|
||||
for area_resource, functions in runner_lib.discovered_samples[area].items():
|
||||
if area_resource != resource and resource != 'all':
|
||||
logger.debug("skipping resource %s", area_resource)
|
||||
continue
|
||||
|
||||
resource_logging_path = runner_lib.enter_resource(area_resource, area_logging_path)
|
||||
|
||||
for run_sample in functions:
|
||||
runner_lib.before_run_sample(run_sample.__name__, resource_logging_path)
|
||||
run_sample(context)
|
||||
runner_lib.after_run_sample(resource_logging_path)
|
||||
|
||||
|
||||
def list_cmd(args, config):
|
||||
template = " <{0}>: {1}"
|
||||
print()
|
||||
print("Available <area>s and resources")
|
||||
print(template.format("all", "all"))
|
||||
for area in runner_lib.discovered_samples.keys():
|
||||
resources = ", ".join(runner_lib.discovered_samples[area].keys())
|
||||
print(template.format(area, resources))
|
||||
print()
|
||||
print("For any area, you can always pass 'all' to run all resource samples")
|
||||
|
||||
|
||||
def run_cmd(args, config):
|
||||
try:
|
||||
auth_token = os.environ['VSTS_PAT']
|
||||
except KeyError:
|
||||
if config['pat']:
|
||||
emit("Using auth token from config file")
|
||||
auth_token = config['pat']
|
||||
else:
|
||||
emit('You must first set the VSTS_PAT environment variable or the `pat` config setting')
|
||||
sys.exit(1)
|
||||
|
||||
if not args.url:
|
||||
if config['url']:
|
||||
args.url = config['url']
|
||||
emit('Using configured URL {0}'.format(args.url))
|
||||
else:
|
||||
emit('No URL configured - pass it on the command line')
|
||||
sys.exit(1)
|
||||
|
||||
args_dict = vars(args)
|
||||
|
||||
main(**args_dict, auth_token=auth_token)
|
||||
|
||||
|
||||
def config_cmd(args, config):
|
||||
template = " {0}: {1}"
|
||||
|
||||
if args.name == 'all':
|
||||
emit("Configured settings")
|
||||
for name in config:
|
||||
emit(template.format(name, config[name]))
|
||||
return
|
||||
|
||||
args.name = args.name.lower()
|
||||
|
||||
if args.set_to:
|
||||
if args.name in config:
|
||||
config[args.name] = args.set_to
|
||||
emit("Setting new value for {0}".format(args.name))
|
||||
emit(template.format(args.name, config[args.name]))
|
||||
config.save()
|
||||
else:
|
||||
emit("There's no setting called {0}".format(args.name))
|
||||
|
||||
elif args.delete:
|
||||
if args.name in config:
|
||||
emit("Deleting {0}; old value was".format(args.name))
|
||||
emit(template.format(args.name, config[args.name]))
|
||||
del config[args.name]
|
||||
config.save()
|
||||
else:
|
||||
emit("There's no setting called {0}".format(args.name))
|
||||
|
||||
else:
|
||||
if args.name in config:
|
||||
emit(template.format(args.name, config[args.name]))
|
||||
else:
|
||||
emit("There's no setting called {0}".format(args.name))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
# main parser
|
||||
parser = argparse.ArgumentParser(description='VSTS Python API samples')
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
# "list"
|
||||
discover_parser = subparsers.add_parser('list')
|
||||
discover_parser.set_defaults(dispatch=list_cmd)
|
||||
|
||||
# "run"
|
||||
run_parser = subparsers.add_parser('run')
|
||||
run_parser.add_argument('area', help='Product area to run samples for, or `all`')
|
||||
run_parser.add_argument('resource', help='Resource to run samples for, or `all`')
|
||||
run_parser.add_argument('-u', '--url', help='Base URL of your VSTS or TFS instance')
|
||||
run_parser.add_argument('-o', '--output-path', help='Root folder to save request/response data',
|
||||
metavar='DIR')
|
||||
run_parser.set_defaults(dispatch=run_cmd)
|
||||
|
||||
# "config"
|
||||
config_parser = subparsers.add_parser('config')
|
||||
config_parser.add_argument('name', help='Name of setting to get or set, or `all` to list all of them')
|
||||
config_parser.add_argument('--set-to', help='New value for setting')
|
||||
config_parser.add_argument('--delete', help='New value for setting', action='store_true')
|
||||
config_parser.set_defaults(dispatch=config_cmd)
|
||||
|
||||
args = parser.parse_args()
|
||||
if 'dispatch' in args:
|
||||
cmd = args.dispatch
|
||||
del args.dispatch
|
||||
cmd(args, Config())
|
||||
else:
|
||||
parser.print_usage()
|
|
@ -0,0 +1,68 @@
|
|||
"""
|
||||
Helper methods moved out of the main runner file.
|
||||
"""
|
||||
import importlib
|
||||
import logging
|
||||
import pathlib
|
||||
import pkgutil
|
||||
|
||||
import http_logging
|
||||
from utils import emit
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
###
|
||||
# Sample discovery
|
||||
###
|
||||
|
||||
SAMPLES_MODULE_NAME = 'samples'
|
||||
|
||||
logger.debug("loading samples module")
|
||||
_samples_module = importlib.import_module(SAMPLES_MODULE_NAME)
|
||||
|
||||
logger.debug('loading all modules in `%s`', SAMPLES_MODULE_NAME)
|
||||
for _, name, _ in pkgutil.iter_modules(_samples_module.__path__):
|
||||
importlib.import_module('%s.%s' % (SAMPLES_MODULE_NAME, name))
|
||||
|
||||
# trim the sample module name off the area names
|
||||
discovered_samples = {
|
||||
area[len(SAMPLES_MODULE_NAME)+1:]: module for area, module in _samples_module.discovered_samples.items()
|
||||
}
|
||||
|
||||
|
||||
###
|
||||
# Logging helpers and so on
|
||||
###
|
||||
|
||||
def enter_area(area, http_logging_path):
|
||||
emit("== %s ==", area)
|
||||
|
||||
if http_logging_path is not None:
|
||||
area_log_dir = pathlib.Path(http_logging_path / area)
|
||||
area_log_dir.mkdir(parents=True, exist_ok=True)
|
||||
return area_log_dir
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def enter_resource(resource, http_logging_path):
|
||||
emit("-- %s --", resource)
|
||||
|
||||
if http_logging_path is not None:
|
||||
resource_log_dir = pathlib.Path(http_logging_path / resource)
|
||||
resource_log_dir.mkdir(parents=True, exist_ok=True)
|
||||
return resource_log_dir
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def before_run_sample(func_name, http_logging_path):
|
||||
if http_logging_path is not None:
|
||||
example_log_file = pathlib.Path(http_logging_path / (func_name + '.json'))
|
||||
http_logging.target = example_log_file.open('w')
|
||||
|
||||
|
||||
def after_run_sample(http_logging_path):
|
||||
if http_logging_path is not None:
|
||||
http_logging.target.close()
|
||||
http_logging.target = None
|
|
@ -0,0 +1,31 @@
|
|||
import logging
|
||||
|
||||
from utils import emit
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
discovered_samples = {}
|
||||
|
||||
|
||||
def resource(decorated_resource):
|
||||
def decorate(sample_func):
|
||||
def run(*args, **kwargs):
|
||||
emit("Running `{0}.{1}`".format(sample_func.__module__, sample_func.__name__))
|
||||
sample_func(*args, **kwargs)
|
||||
|
||||
run.__name__ = sample_func.__name__
|
||||
|
||||
if sample_func.__module__ not in discovered_samples:
|
||||
logger.debug("Discovered area `%s`", sample_func.__module__)
|
||||
discovered_samples[sample_func.__module__] = {}
|
||||
|
||||
area_samples = discovered_samples[sample_func.__module__]
|
||||
if decorated_resource not in area_samples:
|
||||
logger.debug("Discovered resource `%s`", decorated_resource)
|
||||
area_samples[decorated_resource] = []
|
||||
|
||||
logger.debug("Discovered function `%s`", sample_func.__name__)
|
||||
area_samples[decorated_resource].append(run)
|
||||
|
||||
return run
|
||||
return decorate
|
|
@ -0,0 +1,22 @@
|
|||
"""
|
||||
Core samples
|
||||
"""
|
||||
import logging
|
||||
|
||||
from samples import resource
|
||||
from utils import emit
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@resource('projects')
|
||||
def get_projects(context):
|
||||
core_client = context.connection.get_client("vsts.core.v4_1.core_client.CoreClient")
|
||||
|
||||
projects = core_client.get_projects()
|
||||
|
||||
for project in projects:
|
||||
emit(project.id + ": " + project.name)
|
||||
|
||||
return projects
|
|
@ -0,0 +1,38 @@
|
|||
"""
|
||||
Git samples.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from samples import resource
|
||||
from utils import emit, find_any_project, find_any_repo
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@resource('repositories')
|
||||
def get_repos(context):
|
||||
project = find_any_project(context)
|
||||
|
||||
git_client = context.connection.get_client("vsts.git.v4_1.git_client.GitClient")
|
||||
|
||||
repos = git_client.get_repositories(project.id)
|
||||
|
||||
for repo in repos:
|
||||
emit(repo.id + ": " + repo.name)
|
||||
|
||||
return repos
|
||||
|
||||
|
||||
@resource('refs')
|
||||
def get_refs(context):
|
||||
repo = find_any_repo(context)
|
||||
|
||||
git_client = context.connection.get_client("vsts.git.v4_1.git_client.GitClient")
|
||||
|
||||
refs = git_client.get_refs(repo.id, repo.project.id)
|
||||
|
||||
for ref in refs:
|
||||
emit(ref.name + ": " + ref.object_id)
|
||||
|
||||
return refs
|
|
@ -0,0 +1,50 @@
|
|||
"""
|
||||
WIT samples
|
||||
"""
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from samples import resource
|
||||
from utils import emit
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@resource('work_items')
|
||||
def get_work_items(context):
|
||||
wit_client = context.connection.get_client(
|
||||
"vsts.work_item_tracking.v4_1.work_item_tracking_client.WorkItemTrackingClient")
|
||||
|
||||
desired_ids = range(1, 51)
|
||||
work_items = wit_client.get_work_items(ids=desired_ids, error_policy="omit")
|
||||
|
||||
for id_, work_item in zip(desired_ids, work_items):
|
||||
if work_item:
|
||||
emit("{0} {1}: {2}".format(work_item.fields['System.WorkItemType'],
|
||||
work_item.id,
|
||||
work_item.fields['System.Title']))
|
||||
else:
|
||||
emit("(work item {0} omitted by server)".format(id_))
|
||||
|
||||
return work_items
|
||||
|
||||
|
||||
@resource('work_items')
|
||||
def get_work_items_as_of(context):
|
||||
wit_client = context.connection.get_client(
|
||||
"vsts.work_item_tracking.v4_1.work_item_tracking_client.WorkItemTrackingClient")
|
||||
|
||||
desired_ids = range(1, 51)
|
||||
as_of_date = datetime.datetime.now() + datetime.timedelta(days=-7)
|
||||
work_items = wit_client.get_work_items(ids=desired_ids, as_of=as_of_date, error_policy="omit")
|
||||
|
||||
for id_, work_item in zip(desired_ids, work_items):
|
||||
if work_item:
|
||||
emit("{0} {1}: {2}".format(work_item.fields['System.WorkItemType'],
|
||||
work_item.id,
|
||||
work_item.fields['System.Title']))
|
||||
else:
|
||||
emit("(work item {0} omitted by server)".format(id_))
|
||||
|
||||
return work_items
|
|
@ -0,0 +1,54 @@
|
|||
"""
|
||||
Utility methods likely to be useful for anyone building samples.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from exceptions import AccountStateError
|
||||
import http_logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def emit(msg, *args):
|
||||
print(msg % args)
|
||||
|
||||
|
||||
def find_any_project(context):
|
||||
logger.debug('finding any project')
|
||||
|
||||
# if we already contains a looked-up project, return it
|
||||
if hasattr(context.runner_cache, 'project'):
|
||||
logger.debug('using cached project %s', context.runner_cache.project.name)
|
||||
return context.runner_cache.project
|
||||
|
||||
with http_logging.temporarily_disabled():
|
||||
core_client = context.connection.get_client("vsts.core.v4_1.core_client.CoreClient")
|
||||
projects = core_client.get_projects()
|
||||
|
||||
try:
|
||||
context.runner_cache.project = projects[0]
|
||||
logger.debug('found %s', context.runner_cache.project.name)
|
||||
return context.runner_cache.project
|
||||
except IndexError:
|
||||
raise AccountStateError('Your account doesn''t appear to have any projects available.')
|
||||
|
||||
|
||||
def find_any_repo(context):
|
||||
logger.debug('finding any repo')
|
||||
|
||||
# if a repo is cached, use it
|
||||
if hasattr(context.runner_cache, 'repo'):
|
||||
logger.debug('using cached repo %s', context.runner_cache.repo.name)
|
||||
return context.runner_cache.repo
|
||||
|
||||
with http_logging.temporarily_disabled():
|
||||
project = find_any_project(context)
|
||||
git_client = context.connection.get_client("vsts.git.v4_1.git_client.GitClient")
|
||||
repos = git_client.get_repositories(project.id)
|
||||
|
||||
try:
|
||||
context.runner_cache.repo = repos[0]
|
||||
return context.runner_cache.repo
|
||||
except IndexError:
|
||||
raise AccountStateError('Project "%s" doesn''t appear to have any repos.' % (project.name,))
|
Загрузка…
Ссылка в новой задаче