Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions bugwarrior/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from lockfile import LockTimeout
from lockfile.pidlockfile import PIDLockFile

from bugwarrior.collect import aggregate_issues, get_service
from bugwarrior.collect import aggregate_issues
from bugwarrior.config import get_config_path, get_keyring, load_config
from bugwarrior.db import get_defined_udas_as_strings, synchronize

Expand Down Expand Up @@ -162,9 +162,7 @@ def targets() -> Iterator[str]:
for service_config in config.service_configs:
for value in dict(service_config).values():
if isinstance(value, str) and '@oracle:use_keyring' in value:
yield get_service(service_config.service).get_keyring_service(
service_config
)
yield service_config.keyring_service


@vault.command()
Expand Down
14 changes: 14 additions & 0 deletions bugwarrior/config/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ class ServiceConfig(_ServiceConfig):
.. _Pydantic: https://docs.pydantic.dev/latest/
"""

KEYRING_SERVICE: typing.ClassVar[str]

# Added before validation (computed field)
service: str
target: str
Expand All @@ -211,6 +213,18 @@ class ServiceConfig(_ServiceConfig):
add_tags: ConfigList = []
static_fields: ConfigList = []

@property
def keyring_service(self) -> str:
# FIXME change this import and move it outside method after rebase
from bugwarrior.collect import get_service

service = get_service(self.service)
if service.API_VERSION < 2:
assert hasattr(service, "get_keyring_service")
return service.get_keyring_service(self) # ty: ignore

return self.KEYRING_SERVICE.format(**self.model_dump())

@model_validator(mode="before")
@classmethod
def compute_templates(cls, values: dict[str, Any]) -> dict[str, Any]:
Expand Down
2 changes: 1 addition & 1 deletion bugwarrior/docs/other-services/api.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Python API v1.0
Python API v2.0
===============

The interfaces documented here are considered stable. All other interfaces
Expand Down
15 changes: 6 additions & 9 deletions bugwarrior/docs/other-services/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Now define an initial configuration schema as follows. Don't worry, we're about

class GitbugConfig(config.ServiceConfig):
service: typing.Literal['gitbug']
KEYRING_SERVICE = 'gitbug://{path}'

path: pathlib.Path

Expand All @@ -93,6 +94,8 @@ The ``service`` attribute is how bugwarrior will know to assign a given section

The ``path`` is the only particular detail required to access our local git-bug instance. You'll likely need additional details such as a username and token to authenticate to the service. Look at how you accessed the API in step 1 and ask yourself which components need to be configurable.

The ``KEYRING_SERVICE`` attribute is a format string that returns a string identifier for secrets in the keyring. Ideally, this string uniquely identifies a given instance of the service when it is possible to have multiple instances of the service configured. Service configuration values may be referenced by field name, such as ``{path}``.

The ``import_labels_as_tags`` and ``port`` attributes create optional configuration fields to allow customization of bugwarrior behavior.

.. note::
Expand Down Expand Up @@ -187,7 +190,7 @@ Now for the main service class which bugwarrior will invoke to fetch issues.
.. code:: python

class GitBugService(Service):
API_VERSION = 1.0
API_VERSION = 2.0
ISSUE_CLASS = GitBugIssue
CONFIG_SCHEMA = GitBugConfig

Expand All @@ -199,10 +202,6 @@ Now for the main service class which bugwarrior will invoke to fetch issues.
port=self.config.port,
annotation_comments=self.main_config.annotation_comments)

@staticmethod
def get_keyring_service(config):
return f'gitbug://{config.path}'

def issues(self):
for issue in self.client.get_issues():
comments = issue.pop('comments')
Expand All @@ -217,11 +216,9 @@ Now for the main service class which bugwarrior will invoke to fetch issues.

yield self.get_issue_for_record(issue)

Here we see three required class attributes and two required methods.

The ``API_VERSION`` is set to the latest, while the other two attributes point to our previously defined classes.
Here we see three required class attributes and one required method.

The ``get_keyring_service`` method returns a string identifier for secrets in the keyring. Ideally, this string uniquely identifies a given instance of the service when it is possible to have multiple instances of the service configured.
The ``API_VERSION`` is set to the latest, while ``ISSUE_CLASS`` and ``CONFIG_SCHEMA`` point to our previously defined classes.

The ``issues`` method is a generator which yields individual issue dictionaries.

Expand Down
13 changes: 3 additions & 10 deletions bugwarrior/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
# spec will cause breakages with older bugwarrior releases.
# MINOR versions signal extensions of the spec which enhance future releases of
# bugwarrior without breaking past releases.
LATEST_API_VERSION = 1.0
LATEST_API_VERSION = 2.0


class URLShortener:
Expand Down Expand Up @@ -283,10 +283,9 @@ def get_secret(self, key: str, login: str = 'nousername') -> str:
applicable.
"""
password = getattr(self.config, key)
keyring_service = self.get_keyring_service(self.config)
if not password or password.startswith("@oracle:"):
password = secrets.get_service_password(
keyring_service, login, oracle=password
self.config.keyring_service, login, oracle=password
)
return password

Expand All @@ -298,7 +297,7 @@ def get_issue_for_record(
:param `record`: Foreign record.
:param `extra`: Computed data which is not directly from the service.
"""
extra = extra if extra is not None else {}
extra = extra or {}
return self.ISSUE_CLASS(record, self.config, self.main_config, extra=extra)

def build_annotations(
Expand Down Expand Up @@ -359,12 +358,6 @@ def issues(self) -> Iterator[T_Issue]:
"""
raise NotImplementedError()

@staticmethod
@abc.abstractmethod
def get_keyring_service(config: schema.ServiceConfig) -> str:
"""Return the keyring name for this service."""
raise NotImplementedError


class Client:
"""Base class for making requests to service API's.
Expand Down
7 changes: 2 additions & 5 deletions bugwarrior/services/azuredevops.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

class AzureDevopsConfig(config.ServiceConfig):
service: Literal['azuredevops']
KEYRING_SERVICE = "azuredevops://{organization}@{host}"
PAT: str
project: EscapedStr
organization: EscapedStr
Expand Down Expand Up @@ -183,7 +184,7 @@ def get_default_description(self) -> str:


class AzureDevopsService(Service[AzureDevopsIssue]):
API_VERSION = 1.0
API_VERSION = 2.0
ISSUE_CLASS = AzureDevopsIssue
CONFIG_SCHEMA = AzureDevopsConfig

Expand Down Expand Up @@ -259,7 +260,3 @@ def issues(self) -> Iterator[AzureDevopsIssue]:
}
issue_obj.extra.update(extra)
yield issue_obj

@staticmethod
def get_keyring_service(config: AzureDevopsConfig) -> str:
return f"azuredevops://{config.organization}@{config.host}"
7 changes: 2 additions & 5 deletions bugwarrior/services/bitbucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class BitbucketConfig(config.ServiceConfig):
filter_merge_requests: Union[bool, Literal['Undefined']] = 'Undefined'

service: Literal['bitbucket']
KEYRING_SERVICE = "bitbucket://{key}/{username}"

username: str

Expand Down Expand Up @@ -80,7 +81,7 @@ def get_default_description(self) -> str:


class BitbucketService(Service[BitbucketIssue]):
API_VERSION = 1.0
API_VERSION = 2.0
ISSUE_CLASS = BitbucketIssue
CONFIG_SCHEMA = BitbucketConfig

Expand Down Expand Up @@ -116,10 +117,6 @@ def __init__(
'headers': {'Authorization': f"Bearer {response['access_token']}"}
}

@staticmethod
def get_keyring_service(config: BitbucketConfig) -> str:
return f"bitbucket://{config.key}/{config.username}"

def filter_repos(self, repo_tag: str) -> bool:
repo = repo_tag.split('/').pop()

Expand Down
7 changes: 2 additions & 5 deletions bugwarrior/services/bts.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

class BTSConfig(config.ServiceConfig):
service: typing.Literal['bts']
KEYRING_SERVICE = 'bts://'

email: pydantic.EmailStr = ''
packages: config.ConfigList = []
Expand Down Expand Up @@ -103,14 +104,10 @@ def get_priority(self) -> config.Priority:


class BTSService(Service[BTSIssue]):
API_VERSION = 1.0
API_VERSION = 2.0
ISSUE_CLASS = BTSIssue
CONFIG_SCHEMA = BTSConfig

@staticmethod
def get_keyring_service(config: BTSConfig) -> str:
return 'bts://'

def _record_for_bug(self, bug: debianbts.Bugreport) -> dict[str, Any]:
return {
'number': bug.bug_num,
Expand Down
7 changes: 2 additions & 5 deletions bugwarrior/services/bz.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def validate_url(value: str) -> str:

class BugzillaConfig(config.ServiceConfig):
service: typing.Literal['bugzilla']
KEYRING_SERVICE = "bugzilla://{username}@{base_uri}"
username: str
base_uri: OptionalSchemeUrl

Expand Down Expand Up @@ -118,7 +119,7 @@ def get_default_description(self) -> str:


class BugzillaService(Service[BugzillaIssue]):
API_VERSION = 1.0
API_VERSION = 2.0
ISSUE_CLASS = BugzillaIssue
CONFIG_SCHEMA = BugzillaConfig

Expand Down Expand Up @@ -158,10 +159,6 @@ def __init__(
password = self.get_secret('password', self.config.username)
self.bz.login(self.config.username, password)

@staticmethod
def get_keyring_service(config: BugzillaConfig) -> str:
return f"bugzilla://{config.username}@{config.base_uri}"

def get_owner(self, issue: dict[str, Any]) -> str:
return issue['assigned_to']

Expand Down
7 changes: 2 additions & 5 deletions bugwarrior/services/clickup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

class ClickupConfig(config.ServiceConfig):
service: typing.Literal["clickup"]
KEYRING_SERVICE = "clickup://"
token: str
team_id: int

Expand Down Expand Up @@ -120,7 +121,7 @@ def parse_timestamp(


class ClickupService(Service[ClickupIssue]):
API_VERSION = 1.0
API_VERSION = 2.0
ISSUE_CLASS = ClickupIssue
CONFIG_SCHEMA = ClickupConfig

Expand All @@ -130,10 +131,6 @@ def __init__(
super().__init__(config, main_config)
self.client = ClickupClient(token=self.get_secret('token'))

@staticmethod
def get_keyring_service(config: ClickupConfig) -> str:
return "clickup://"

def is_assigned(self, issue: dict) -> bool:
if not self.config.only_if_assigned:
return True
Expand Down
7 changes: 2 additions & 5 deletions bugwarrior/services/deck.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

class NextcloudDeckConfig(config.ServiceConfig):
service: typing.Literal['deck']
KEYRING_SERVICE = 'deck://{username}@{base_uri}'
base_uri: config.StrippedTrailingSlashUrl
username: str

Expand Down Expand Up @@ -126,7 +127,7 @@ def get_default_description(self) -> str:


class NextcloudDeckService(Service[NextcloudDeckIssue]):
API_VERSION = 1.0
API_VERSION = 2.0
ISSUE_CLASS = NextcloudDeckIssue
CONFIG_SCHEMA = NextcloudDeckConfig

Expand All @@ -141,10 +142,6 @@ def __init__(
password=self.config.password,
)

@staticmethod
def get_keyring_service(config: NextcloudDeckConfig) -> str:
return f'deck://{config.username}@{config.base_uri}'

def get_owner(self, issue: NextcloudDeckIssue) -> str | None:
rec = issue.record
if rec.get('assignedUsers'):
Expand Down
7 changes: 2 additions & 5 deletions bugwarrior/services/gerrit.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

class GerritConfig(config.ServiceConfig):
service: typing.Literal['gerrit']
KEYRING_SERVICE = "gerrit://{base_uri}"
base_uri: config.StrippedTrailingSlashUrl
username: str
password: str
Expand Down Expand Up @@ -72,7 +73,7 @@ def get_default_description(self) -> str:


class GerritService(Service[GerritIssue]):
API_VERSION = 1.0
API_VERSION = 2.0
ISSUE_CLASS = GerritIssue
CONFIG_SCHEMA = GerritConfig

Expand Down Expand Up @@ -102,10 +103,6 @@ def __init__(
self.config.username, self.password
)

@staticmethod
def get_keyring_service(config: GerritConfig) -> str:
return f"gerrit://{config.base_uri}"

def issues(self) -> Iterator[GerritIssue]:
# Construct the whole url by hand here, because otherwise requests will
# percent-encode the ':' characters, which gerrit doesn't like.
Expand Down
7 changes: 2 additions & 5 deletions bugwarrior/services/gitbug.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

class GitBugConfig(config.ServiceConfig):
service: Literal['gitbug']
KEYRING_SERVICE = 'gitbug://{path}'

path: config.ExpandedPath

Expand Down Expand Up @@ -138,7 +139,7 @@ def get_default_description(self) -> str:


class GitBugService(Service[GitBugIssue]):
API_VERSION = 1.0
API_VERSION = 2.0
ISSUE_CLASS = GitBugIssue
CONFIG_SCHEMA = GitBugConfig

Expand All @@ -153,10 +154,6 @@ def __init__(
annotation_comments=self.main_config.annotation_comments,
)

@staticmethod
def get_keyring_service(config: GitBugConfig) -> str:
return f'gitbug://{config.path}'

def issues(self) -> Iterator[GitBugIssue]:
for issue in self.client.get_issues():
comments = issue.pop('comments')
Expand Down
7 changes: 2 additions & 5 deletions bugwarrior/services/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class GithubConfig(config.ServiceConfig):

# strictly required
service: typing.Literal['github']
KEYRING_SERVICE = "github://{login}@{host}/{username}"
login: str
token: str

Expand Down Expand Up @@ -293,7 +294,7 @@ def get_default_description(self) -> str:


class GithubService(Service[GithubIssue]):
API_VERSION = 1.0
API_VERSION = 2.0
ISSUE_CLASS = GithubIssue
CONFIG_SCHEMA = GithubConfig

Expand All @@ -305,10 +306,6 @@ def __init__(
auth = {'token': self.get_secret('token', self.config.login)}
self.client = GithubClient(self.config.host, auth)

@staticmethod
def get_keyring_service(config: GithubConfig) -> str:
return f"github://{config.login}@{config.host}/{config.username}"

def get_owned_repo_issues(self, tag: str) -> GithubIssueMap:
"""Grab all the issues"""
issues = {}
Expand Down
Loading
Loading