PKU;H gocdpb/goserver_config.pyimport copy from xml.etree import ElementTree __author__ = 'magnusl' class CruiseTree(ElementTree.ElementTree): """ A thin layer on top of the cruise-config.xml used by the Go server. """ @classmethod def fromstring(cls, text): return cls(ElementTree.fromstring(text)) def tostring(self): self.indent(self.getroot()) return ElementTree.tostring(self.getroot()) def config_subset_tostring(self): """ See GoProxy.set_test_settings_xml() """ root = copy.deepcopy(self).getroot() for child in list(root): if child.tag not in ('pipelines', 'templates', 'environments'): root.remove(child) self.indent(root) return ElementTree.tostring(root) @classmethod def indent(cls, elem, level=0): """ Fredrik Lundh's standard recipe. (Why isn't this in xml.etree???) """ i = "\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): elem.text = i + " " if not elem.tail or not elem.tail.strip(): elem.tail = i for elem in elem: cls.indent(elem, level + 1) if not elem.tail or not elem.tail.strip(): elem.tail = i else: if level and (not elem.tail or not elem.tail.strip()): elem.tail = i def set_test_settings_xml(self, test_settings_xml): """ Replace parts of the Go server config for test purposes The main sections in the config are: - server - repositories - pipelines * N - templates - environments - agents We want to replace the sections pipelines*, templates and environments. Let server and agents stay as usual. We've never used repositories so far. """ root = self.getroot() self.drop_sections_to_be_replaced(root) test_settings = CruiseTree().parse(test_settings_xml) ix = self.place_for_test_settings(root) for element_type in ('environments', 'templates', 'pipelines'): for elem in reversed(test_settings.findall(element_type)): root.insert(ix, elem) def drop_sections_to_be_replaced(self, root): for tag in ('pipelines', 'templates', 'environments'): for element_to_drop in self.findall(tag): root.remove(element_to_drop) @staticmethod def place_for_test_settings(root): ix = 0 # Silence lint about using ix after the loop. root != [] for ix, element in enumerate(list(root)): if element.tag == 'agents': break return ixPKrWH00gocdpb/test_gocdrepos.pyimport gocdpb import sys gocdpb.repos(sys.argv)PKVqWHԑwgocdpb/gocdpb.py#!/usr/bin/python -tt # coding:utf-8 import sys import getpass import argparse import requests from goserver_adapter import Goserver from gocd_settings import JsonSettings, YamlSettings, Pipeline def list2dict(list_of_pairs): return dict(tuple(pair.split('=', 1) for pair in list_of_pairs or [])) def add_secrets_to_config(config, password_parameters): for password_parameter in password_parameters or []: config[password_parameter] = getpass.getpass(password_parameter + ': ') def get_json_settings(path): if path.startswith('http'): response = requests.get(path) assert response.status_code == 200 return response.text else: return open(path).read() def repos(args=sys.argv): argparser = argparse.ArgumentParser( description="Recursively fetch all source code revisions used in a pipeline build." ) argparser.add_argument( "material_revisions", help="pipeline/instance to start at." ) argparser.add_argument( "-f", "--format", choices=['semicolon', 'json'], default='json', help="Format for output from --material-revisions." ) go, pargs = init_run(argparser, args) Pipeline(pargs.material_revisions, go, pargs.format).print_recursive_repos() def main(args=sys.argv): argparser = argparse.ArgumentParser( description="Add pipeline to Go CD server." ) main_action_group = argparser.add_mutually_exclusive_group() main_action_group.add_argument( "-j", "--json-settings", help="Read json file / url with settings for GoCD pipeline." ) main_action_group.add_argument( "-y", "--yaml-settings", type=argparse.FileType('r'), help="Read yaml files with parameters for GoCD pipeline." ) argparser.add_argument( "-D", "--define", action="append", help="Define setting parameter on command line." ) argparser.add_argument( "--dump-test-config", type=argparse.FileType('w'), help="Copy of some sections of new GoCD configuration XML file." ) argparser.add_argument( "-d", "--dump", type=argparse.FileType('w'), help="Copy of new GoCD configuration XML file." ) go, pargs = init_run(argparser, args) if pargs.json_settings: json_settings = get_json_settings(pargs.json_settings) JsonSettings(json_settings, list2dict(pargs.define) ).server_operations(go) if pargs.yaml_settings is not None: YamlSettings(pargs.yaml_settings, list2dict(pargs.define) ).server_operations(go) if pargs.dump is not None: go.init() envelope = '\n%s' pargs.dump.write(envelope % go.cruise_xml) if pargs.dump_test_config is not None: go.init() envelope = '\n%s' pargs.dump_test_config.write(envelope % go.cruise_xml_subset) def init_run(argparser, args): argparser.add_argument( "-v", "--verbose", action="store_true", help="Write status of created pipeline." ) argparser.add_argument( "-c", "--config", type=argparse.FileType('r'), help="Yaml file with configuration." ) argparser.add_argument( "-C", "--config-param", action="append", help="Define config parameter on command line." ) argparser.add_argument( "-P", "--password-prompt", action="append", help="Prompt for config parameter without echo." ) argparser.add_argument( "--set-test-config", type=argparse.FileType('r'), help="Set some sections in config first. (For test setup.)" ) pargs = argparser.parse_args(args[1:]) extra_config = list2dict(pargs.config_param) add_secrets_to_config(extra_config, pargs.password_prompt) go = Goserver(pargs.config, pargs.verbose, extra_config) try: go.check_config() except KeyError as error: print "Missing {} in configuration.".format(error) sys.exit(1) go.init() if pargs.set_test_config is not None: go.tree.set_test_settings_xml(pargs.set_test_config) go.upload_config() return go, pargs if __name__ == '__main__': main() PKjWH66gocdpb/gocd_settings.pyimport re import sys import json import os.path from collections import OrderedDict, defaultdict from xml.etree import ElementTree import yaml from jinja2 import Template class JsonSettings(object): """ A json file passed to the this class should have the following structure: [ { "": , }, ] See README.md for details. """ def __init__(self, settings_file, extra_settings): self.list = None self.load_file(settings_file, extra_settings) self.pipeline_names = [] self.pipeline_stage_names = [] self.pipeline_group_map = {} def load_file(self, settings_file, extra_settings): self.load_template(settings_file, extra_settings) def load_template(self, template_data, parameters): template = Template(template_data) data = self.get_default_parameters() data.update(parameters) self.list = json.loads(template.render(data), object_pairs_hook=OrderedDict) @staticmethod def get_default_parameters(git_config_path='.git/config'): data = {} if os.path.exists(git_config_path): for row in open(git_config_path): if row.strip().startswith('url = '): data['repo_url'] = row.split('=')[1].strip() data['repo_name'] = os.path.basename(os.getcwd()) return data def server_operations(self, go): for operation in self.list: if "create-a-pipeline" in operation: go.create_a_pipeline(operation["create-a-pipeline"]) self.pipeline_names.append(operation["create-a-pipeline"]["pipeline"]["name"]) self.pipeline_stage_names.append([ stage["name"] for stage in operation["create-a-pipeline"]["pipeline"].get("stages") or [] ]) if "clone-pipelines" in operation: self.clone_pipelines(go, operation["clone-pipelines"]) if self.pipeline_names and "environment" in operation: go.init() self.update_environment(go.tree, operation) if go.need_to_upload_config: go.upload_config() if "add-downstream-dependencies" in operation: dependency_updates = operation["add-downstream-dependencies"] for dependency_update in dependency_updates: downstream_name = dependency_update["name"] etag, pipeline = go.get_pipeline_config(downstream_name) # If this pipeline uses a template, we need to use that!!! self.add_downstream_dependencies(pipeline, dependency_update) go.edit_pipeline_config(downstream_name, etag, pipeline) for pipeline_name in self.pipeline_names: if "unpause" in operation and operation["unpause"]: go.unpause(pipeline_name) if go.verbose: status = go.get_pipeline_status(pipeline_name) print json.dumps(status, indent=4, sort_keys=True) def clone_pipelines(self, go, operation): """ For now, this is fairly limited. Given a "FIND-group" and a "CREATE-group" in the operation, we use re.sub with FIND-name and REPLACE-name in the pipeline to make a new pipeline in the group "REPLACE-name" with a new name for each pipeline in group "FIND-name". The new pipelines will use the git branch indicated by "REPLACE-branch" for their Git material. """ find_group = operation["FIND-group"] all_groups = go.get_pipeline_groups() for pipeline_group in all_groups: for pipeline in pipeline_group["pipelines"]: self.pipeline_group_map[pipeline['name']] = pipeline_group['name'] for pipeline, group in self.pipeline_group_map.items(): if group == find_group: name = self.clone_pipeline(go, operation, pipeline) if name: self.pipeline_names.append(name) for pipeline_name in self.pipeline_names: self.fix_pipeline(go, operation, pipeline_name) def clone_pipeline(self, go, operation, old_name): create_group = operation["CREATE-group"] find_name = operation['pipeline']["FIND-name"] replace_name = operation['pipeline']["REPLACE-name"] etag, pipeline = go.get_pipeline_config(old_name) new_name = re.sub(find_name, replace_name, old_name) if new_name is None: return pipeline['name'] = new_name new_pipeline = dict(group=create_group, pipeline=pipeline) old_material = pipeline["materials"][:] pipeline["materials"] = [] for actual_material in old_material: for op_material in operation['pipeline']['materials']: if actual_material["type"] == op_material["type"]: ok = self._update_material(actual_material, op_material) if ok: pipeline["materials"].append(actual_material) go.create_a_pipeline(new_pipeline) return new_name def fix_pipeline(self, go, operation, name): etag, pipeline = go.get_pipeline_config(name) for actual_material in pipeline["materials"]: for op_material in operation['pipeline']['materials']: if actual_material["type"] == op_material["type"] == 'dependency': self._fix_dependencies(actual_material, op_material) go.edit_pipeline_config(name, etag, pipeline) def _update_material(self, actual_material, op_material): """ This method is called on the material in pipelines we clone. It should copy source code repositories and update their branch. It should copy the matching dependency material, but we should postpone updates of the names, until all pipelines have been cloned to avoid references to not yet created pipelines. """ if actual_material["type"] == 'git': actual_material["attributes"]["branch"] = op_material["attributes"]["REPLACE-branch"] return True elif actual_material["type"] == 'dependency': find_pipeline = op_material['attributes']["FIND-pipeline"] pipeline_name = actual_material["attributes"]["pipeline"] if not re.search(find_pipeline, pipeline_name): return False find_group = op_material['attributes']["FIND-group"] if self.pipeline_group_map[pipeline_name] == find_group: return True return False @staticmethod def _fix_dependencies(actual_material, op_material): find_pipeline = op_material['attributes']["FIND-pipeline"] replace_pipeline = op_material['attributes']["REPLACE-pipeline"] new_name = re.sub(find_pipeline, replace_pipeline, actual_material["attributes"]["pipeline"]) if new_name: actual_material["attributes"]["pipeline"] = new_name def add_downstream_dependencies(self, pipeline, update): if "material" in update: pipeline["materials"].append(update["material"]) if "task" in update: self.ensure_dependency_material(pipeline) if "stages" in pipeline: job = self.get_job(pipeline, update) job["tasks"].insert(0, update["task"]) else: sys.stderr.write("Adding tasks to template not supported!\n") def ensure_dependency_material(self, pipeline): for material in pipeline['materials']: if material['type'] == 'dependency' and material['attributes']['pipeline']: return # Expected dependency material not found. Add default. if self.pipeline_stage_names and (len(self.pipeline_stage_names[-1]) == 1): pipeline['materials'].append( { "type": "dependency", "attributes": { "pipeline": self.pipeline_names[-1], "stage": self.pipeline_stage_names[-1][0], "auto_update": True } } ) else: raise ValueError('Explicit dependency material is needed unless' ' there is exactly one stage in new pipeline') @staticmethod def get_job(pipeline, update): if "stage" in update: for stage in pipeline["stages"]: if stage["name"] == update["stage"]: break else: stage = pipeline["stages"][0] if "job" in update: for job in stage["jobs"]: if job["name"] == update["job"]: break else: job = stage["jobs"][0] return job def update_environment(self, configuration, operation): """ If the setting names an environment, the pipelines in the setting, should be assigned to that environment in the cruise-config. """ conf_environments = configuration.find('environments') if conf_environments is None: print "No environments section in configuration." return op_env_name = operation.get('environment') if not op_env_name: return for conf_environment in conf_environments.findall('environment'): if conf_environment.get('name') == op_env_name: for name in self.pipeline_names: self._set_pipeline_in_environment(name, conf_environment) break @staticmethod def _set_pipeline_in_environment(name, conf_environment): conf_pipelines = conf_environment.find('pipelines') if conf_pipelines is None: conf_pipelines = ElementTree.SubElement( conf_environment, 'pipelines') # TODO: Check if already there? conf_pipeline = ElementTree.SubElement(conf_pipelines, 'pipeline') conf_pipeline.set('name', name) class YamlSettings(JsonSettings): """ A YamlSettings object is initiated with a yaml file, which has a 'path', indicating a json template file, and 'parameters', where we find a dictionary that we can pass to the template. This dictionary might contains parameters that override default parameters in the base class. """ def load_file(self, settings_file, extra_settings): """ Find the json template and parameter in the yaml file, render the template, and pass it to the super class. """ settings = yaml.load(settings_file) template_path = settings['path'] parameters = settings['parameters'] parameters.update(extra_settings) self.load_template(open(template_path).read(), parameters) def last_modification(modifications): return sorted(modifications, key=lambda rev: rev['modified_time'])[-1] class SourceMaterial(object): def __init__(self, material_revision): self.__type = material_revision['material']['type'] self.__description = material_revision['material']['description'] last_mod = last_modification(material_revision['modifications']) self.__revision = last_mod['revision'] def as_dict(self, **kwargs): data = dict(type=self.__type, description=self.__description, revision=self.__revision) data.update(kwargs) return data def __str__(self): return "; ".join((self.__type, self.__description, self.__revision)) def __hash__(self): return hash(self.__revision) def __eq__(self, other): return self.__revision == other.__revision class Pipeline(object): def __init__(self, pipeline_instance, go, output_format): self.pipeline, self.instance = pipeline_instance.split('/')[:2] self.go = go self.format = output_format self.upstreams = [] self.source_repos = [] self.recursive_repos = defaultdict(set) def print_recursive_repos(self): self.prepare_recursive_repos() self.collect_recursive_repos() if self.format == 'json': repos = [repo.as_dict(pipelines=[dict(zip(('name', 'counter'), pl)) for pl in pipelines]) for repo, pipelines in self.recursive_repos.items()] print json.dumps(repos, indent=4, sort_keys=True) elif self.format == 'semicolon': for repo, pipelines in self.recursive_repos.items(): print "%s; %s" % (repo, ", ".join(["%s/%s" % (p, i) for p, i in pipelines])) else: raise TypeError("Don't know how to print in format: {}".format(self.format)) def prepare_recursive_repos(self): pipeline_instance = self.go.get_pipeline_instance(self.pipeline, self.instance) for material_revision in pipeline_instance['build_cause']['material_revisions']: if material_revision['material']['type'] == 'Pipeline': last_mod = last_modification(material_revision['modifications']) upstream_pipeline = Pipeline(last_mod['revision'], self.go, self.format) self.upstreams.append(upstream_pipeline) upstream_pipeline.prepare_recursive_repos() else: self.source_repos.append(SourceMaterial(material_revision)) def collect_recursive_repos(self): for upstream in self.upstreams: upstream.collect_recursive_repos() for repo in upstream.recursive_repos: self.recursive_repos[repo].update(upstream.recursive_repos[repo]) for repo in self.source_repos: self.recursive_repos[repo].add((self.pipeline, self.instance)) PK\UGgocdpb/__init__.pyPKYH=gocdpb/goserver_adapter.pyimport sys import json import yaml import requests from collections import OrderedDict from goserver_config import CruiseTree class Goserver(object): """ Manages HTTP communication with the Go server. """ config_xml_rest_path = "/go/admin/restful/configuration/file/{}/xml" def __init__(self, config, verbose, config_overrides): self.__config = {} if config is not None: self.__config.update(yaml.load(config)) self.__config.update(config_overrides) self.verbose = verbose self._cruise_config_md5 = None self.tree = None self._initial_xml = None self.need_to_download_config = True def check_config(self): for param in ( 'url', ): self.__config[param] def init(self): """ Fetch configuration from Go server """ if self.need_to_download_config: self.tree = CruiseTree.fromstring(self.xml_from_url()) self._initial_xml = self.cruise_xml self.need_to_download_config = False @property def need_to_upload_config(self): return self.cruise_xml != self._initial_xml @property def __auth(self): if 'username' in self.__config: return self.__config['username'], self.__config['password'] @property def cruise_xml(self): return self.tree.tostring() @property def cruise_xml_subset(self): return self.tree.config_subset_tostring() def request(self, action, path, **kwargs): action = action.upper() if self._changing_call(action): # If we change via REST API, we need to fetch the config # again if we need to understand the state. # If we uploaded the config XML, we need to fetch it # again to get a new md5 checksum in case we want to # change some more... self.need_to_download_config = True url = self.__config['url'] + path if self.__auth: kwargs['auth'] = self.__auth response = requests.request(action, url, **kwargs) if response.status_code != 200: sys.stderr.write("Failed to {} {}\n".format(action, path)) sys.stderr.write("status-code: {}\n".format(response.status_code)) sys.stderr.write("text: {}\n".format(response.text)) return response @staticmethod def _changing_call(action): return action not in ('HEAD', 'GET') def xml_from_url(self): action = 'GET' path = self.config_xml_rest_path.format(action) response = self.request(action, path) if response.status_code != 200: raise RuntimeError(str(response.status_code)) self._cruise_config_md5 = response.headers['x-cruise-config-md5'] return response.text def create_a_pipeline(self, pipeline): """ Add a pipeline to the Go server configuration using the REST API: https://api.go.cd/current/#create-a-pipeline :param pipeline: Json object as describe in API above. """ path = "/go/api/admin/pipelines" data = json.dumps(pipeline) headers = { 'Accept': 'application/vnd.go.cd.v1+json', 'Content-Type': 'application/json' } response = self.request('post', path, data=data, headers=headers) if response.status_code != 200: raise RuntimeError(str(response.status_code)) def get_pipeline_config(self, pipeline_name): path = "/go/api/admin/pipelines/" + pipeline_name headers = { 'Accept': 'application/vnd.go.cd.v1+json' } response = self.request('get', path, headers=headers) if response.status_code != 200: raise RuntimeError(str(response.status_code)) json_data = json.loads(response.text.replace("\\'", "'"), object_pairs_hook=OrderedDict) etag = response.headers['etag'] return etag, json_data def edit_pipeline_config(self, pipeline_name, etag, pipeline): path = "/go/api/admin/pipelines/" + pipeline_name data = json.dumps(pipeline) headers = { 'Accept': 'application/vnd.go.cd.v1+json', 'Content-Type': 'application/json', 'If-Match': etag } response = self.request('put', path, data=data, headers=headers) if response.status_code != 200: print response.text raise RuntimeError(str(response.status_code)) def unpause(self, pipeline_name): path = "/go/api/pipelines/" + pipeline_name + "/unpause" self.request('post', path) def get_pipeline_status(self, pipeline_name): path = "/go/api/pipelines/" + pipeline_name + "/status" headers = { 'Accept': 'application/json' } response = self.request('get', path, headers=headers) if response.status_code != 200: raise RuntimeError(str(response.status_code)) json_data = json.loads(response.text.replace("\\'", "'"), object_pairs_hook=OrderedDict) return json_data def get_pipeline_groups(self): path = "/go/api/config/pipeline_groups" headers = { 'Accept': 'application/json' } response = self.request('get', path, headers=headers) if response.status_code != 200: raise RuntimeError(str(response.status_code)) json_data = json.loads(response.text.replace("\\'", "'"), object_pairs_hook=OrderedDict) return json_data def get_pipeline_instance(self, pipeline, instance): path = "/go/api/pipelines/" + pipeline + "/instance/" + instance headers = { 'Accept': 'application/json' } response = self.request('get', path, headers=headers) if response.status_code != 200: raise RuntimeError(str(response.status_code)) json_data = json.loads(response.text.replace("\\'", "'"), object_pairs_hook=OrderedDict) return json_data def upload_config(self): """ This method pushes a new cruise-config.xml to the go server. It's used when there is no REST API for the changes we want to do. Make sure to refresh the cruise-config by using the .init() method if the Go server config was just changed through the REST API. """ if self.cruise_xml == self._initial_xml: print "No changes done. Not uploading config." else: data = {'xmlFile': self.cruise_xml, 'md5': self._cruise_config_md5} action = 'POST' response = self.request(action, self.config_xml_rest_path.format(action), data=data) if response.status_code != 200: sys.stderr.write("status-code: %s\n" % response.status_code) # GoCD produces broken JSON???, see # https://github.com/gocd/gocd/issues/1472 json_data = json.loads(response.text.replace("\\'", "'"), object_pairs_hook=OrderedDict) sys.stderr.write("result: %s\n" % json_data["result"]) sys.stderr.write( "originalContent:\n%s\n" % json_data["originalContent"]) raise RuntimeError(response.status_code) PK#tZHX bb"gocdpb-3.dist-info/DESCRIPTION.rstThe Go CD Pipeline Builder is designed to have the same function in the Go CD ecology, as the Jenkins Job Builder has in the Jenkins ecology. Given a (git) repository and appropriate configuration, it should be able to add a suitable pipeline to a Go-server. The current version does not trigger on git events. It is simply a command line driven tool. PK#tZHOO#gocdpb-3.dist-info/entry_points.txt[console_scripts] gocdpb = gocdpb.gocdpb:main gocdrepos = gocdpb.gocdpb:repos PK#tZHsXX gocdpb-3.dist-info/metadata.json{"classifiers": ["Programming Language :: Python :: 2.7", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Topic :: Software Development :: Build Tools", "Environment :: Console"], "extensions": {"python.commands": {"wrap_console": {"gocdpb": "gocdpb.gocdpb:main", "gocdrepos": "gocdpb.gocdpb:repos"}}, "python.details": {"contacts": [{"email": "magnus@thinkware.se; ", "name": "Magnus Lyck\u00e5", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/magnus-lycka/gocd-pipeline-builder"}}, "python.exports": {"console_scripts": {"gocdpb": "gocdpb.gocdpb:main", "gocdrepos": "gocdpb.gocdpb:repos"}}}, "extras": [], "generator": "bdist_wheel (0.26.0)", "keywords": ["continuous", "deployment", "integration", "build", "automation", "gocd"], "license": "MIT", "metadata_version": "2.0", "name": "gocdpb", "run_requires": [{"requires": ["PyYAML", "jinja2", "requests"]}], "summary": "Configure GoCD pipeline from the commandline.", "version": "3"}PK#tZH8 gocdpb-3.dist-info/top_level.txtgocdpb PK#tZH''\\gocdpb-3.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py2-none-any PK#tZHV[00gocdpb-3.dist-info/METADATAMetadata-Version: 2.0 Name: gocdpb Version: 3 Summary: Configure GoCD pipeline from the commandline. Home-page: https://github.com/magnus-lycka/gocd-pipeline-builder Author: Magnus Lyckå Author-email: magnus@thinkware.se; License: MIT Keywords: continuous deployment integration build automation gocd Platform: UNKNOWN Classifier: Programming Language :: Python :: 2.7 Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: Topic :: Software Development :: Build Tools Classifier: Environment :: Console Requires-Dist: PyYAML Requires-Dist: jinja2 Requires-Dist: requests The Go CD Pipeline Builder is designed to have the same function in the Go CD ecology, as the Jenkins Job Builder has in the Jenkins ecology. Given a (git) repository and appropriate configuration, it should be able to add a suitable pipeline to a Go-server. The current version does not trigger on git events. It is simply a command line driven tool. PK#tZHgocdpb-3.dist-info/RECORDgocdpb/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 gocdpb/gocd_settings.py,sha256=RqPsvzSw3kDtq_YcgrmCnrUbo3mQA2JqRGfP9SxLmDI,14029 gocdpb/gocdpb.py,sha256=SOJ8ftv2B1-2r5GlikIXYtvZV51qwC717DgIXR2fabA,4378 gocdpb/goserver_adapter.py,sha256=dSOkw6onRnAYOEd_rSDOtpXQ9j_iOaK1VkgS7KrOaqs,7571 gocdpb/goserver_config.py,sha256=H7bEN1d_eoP4qwbHYAppB1dU0s_G7-8HQISZHOQgKMY,2770 gocdpb/test_gocdrepos.py,sha256=jQFYGCGvSxBynoP4ErZMQv-Ia8D8EpvT5og4_yVHdQ0,48 gocdpb-3.dist-info/DESCRIPTION.rst,sha256=Iwu4GmG1uuC_zbUCchrCk5CEM6IMPJEHaVqWcv0eMR4,354 gocdpb-3.dist-info/METADATA,sha256=lmwG9v86BqhrWd-ystZHzkF71l1wxNfro4D9e9sHvcc,1072 gocdpb-3.dist-info/RECORD,, gocdpb-3.dist-info/WHEEL,sha256=JTb7YztR8fkPg6aSjc571Q4eiVHCwmUDlX8PhuuqIIE,92 gocdpb-3.dist-info/entry_points.txt,sha256=SjNkoEk_pKWt_SphjPm4gOP_XVW9-zR7AylCWq86Dis,79 gocdpb-3.dist-info/metadata.json,sha256=l-ZEM7FemjRJpJLau47F_uA_rWuH1gY9cF2NeG0_37c,1112 gocdpb-3.dist-info/top_level.txt,sha256=sbv3DlHhegYXVulQf5lR2lk8wH-KENlFyvPonpfQ9v4,7 PKU;H gocdpb/goserver_config.pyPKrWH00 gocdpb/test_gocdrepos.pyPKVqWHԑwo gocdpb/gocdpb.pyPKjWH66gocdpb/gocd_settings.pyPK\UGSgocdpb/__init__.pyPKYH=Sgocdpb/goserver_adapter.pyPK#tZHX bb"qgocdpb-3.dist-info/DESCRIPTION.rstPK#tZHOO#Vsgocdpb-3.dist-info/entry_points.txtPK#tZHsXX sgocdpb-3.dist-info/metadata.jsonPK#tZH8 |xgocdpb-3.dist-info/top_level.txtPK#tZH''\\xgocdpb-3.dist-info/WHEELPK#tZHV[00Sygocdpb-3.dist-info/METADATAPK#tZH}gocdpb-3.dist-info/RECORDPK