Port samples from private repository

This commit is contained in:
Matt Cooper 2018-01-09 07:58:29 -05:00
Родитель 039789d80c
Коммит a1b676a3ee
18 изменённых файлов: 972 добавлений и 9 удалений

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

@ -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

141
API Samples.ipynb Normal file
Просмотреть файл

@ -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
Просмотреть файл

@ -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).

58
contribute.md Normal file
Просмотреть файл

@ -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)

4
requirements.jupyter.txt Normal file
Просмотреть файл

@ -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

2
requirements.txt Normal file
Просмотреть файл

@ -0,0 +1,2 @@
# if you just want the command-line samples experience, use this requirements file
vsts==0.1.0b2

3
src/.flake8 Normal file
Просмотреть файл

@ -0,0 +1,3 @@
[flake8]
exclude = __pycache__
max-line-length = 120

58
src/config.py Normal file
Просмотреть файл

@ -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))

8
src/exceptions.py Normal file
Просмотреть файл

@ -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

19
src/hacks.py Normal file
Просмотреть файл

@ -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)

106
src/http_logging.py Normal file
Просмотреть файл

@ -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)

189
src/runner.py Normal file
Просмотреть файл

@ -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()

68
src/runner_lib.py Normal file
Просмотреть файл

@ -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

31
src/samples/__init__.py Normal file
Просмотреть файл

@ -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

22
src/samples/core.py Normal file
Просмотреть файл

@ -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

38
src/samples/git.py Normal file
Просмотреть файл

@ -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

54
src/utils.py Normal file
Просмотреть файл

@ -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,))