66import abc
77from collections .abc import Iterable , Iterator
88import datetime
9+ from functools import cache
10+ from importlib .metadata import entry_points
911import logging
1012import math
1113import os
1214import re
13- from typing import Any , Generic , Optional , TypeVar
15+ from typing import Any , Generic , NamedTuple , Optional , TypeVar
1416import zoneinfo
1517
1618from dateutil .parser import parse as parse_date
1719import dogpile .cache
1820from jinja2 import Template
1921import requests
2022
21- from bugwarrior .config import schema , secrets
22- from bugwarrior .types import TaskwarriorData
23+ from bugwarrior .config . schema import MainSectionConfig , Priority , ServiceConfig
24+ from bugwarrior .config . secrets import get_service_password
2325
2426log = logging .getLogger (__name__ )
2527
4143LATEST_API_VERSION = 1.0
4244
4345
46+ class CollectedIssue (NamedTuple ):
47+ taskwarrior_data : dict [str , Any ]
48+ target : str
49+ identifier : str
50+
51+
52+ class CollectionErrorData (NamedTuple ):
53+ error_message : str
54+ target : str
55+
56+
4457class URLShortener :
4558 _instance = None
4659
@@ -57,7 +70,7 @@ def shorten(self, url: str) -> str:
5770 return requests .get (base , params = dict (url = url )).text .strip ()
5871
5972
60- def get_processed_url (main_config : schema . MainSectionConfig , url : str ) -> str :
73+ def get_processed_url (main_config : MainSectionConfig , url : str ) -> str :
6174 """Returns a URL with conditional processing.
6275
6376 If the following config key are set:
@@ -107,22 +120,22 @@ class Issue(abc.ABC):
107120 def __init__ (
108121 self ,
109122 foreign_record : dict [str , Any ],
110- config : schema . ServiceConfig ,
111- main_config : schema . MainSectionConfig ,
123+ config : ServiceConfig ,
124+ main_config : MainSectionConfig ,
112125 extra : dict [str , Any ],
113126 ) -> None :
114127 #: Data retrieved from the external service.
115128 self .record = foreign_record
116129 #: An object whose attributes are this service's configuration values.
117- self .config : schema . ServiceConfig = config
130+ self .config = config
118131 #: An object whose attributes are the
119132 #: :ref:`common_configuration:Main Section` configuration values.
120- self .main_config : schema . MainSectionConfig = main_config
133+ self .main_config = main_config
121134 #: Data computed by the :class:`Service` class.
122135 self .extra = extra
123136
124137 @abc .abstractmethod
125- def to_taskwarrior (self ) -> TaskwarriorData :
138+ def to_taskwarrior (self ) -> dict [ str , Any ] :
126139 """Transform a foreign record into a taskwarrior dictionary."""
127140 raise NotImplementedError ()
128141
@@ -167,7 +180,7 @@ def get_tags_from_labels(
167180
168181 return tags
169182
170- def get_priority (self ) -> schema . Priority :
183+ def get_priority (self ) -> Priority :
171184 """Return the priority of this issue, falling back to ``default_priority`` configuration."""
172185 return self .PRIORITY_MAP .get (
173186 self .record .get ('priority' ), self .config .default_priority
@@ -253,11 +266,9 @@ class Service(abc.ABC, Generic[T_Issue]):
253266 #: Which class should this service instantiate for holding these issues?
254267 ISSUE_CLASS : type [T_Issue ]
255268 #: Which class defines this service's configuration options?
256- CONFIG_SCHEMA : type [schema . ServiceConfig ]
269+ CONFIG_SCHEMA : type [ServiceConfig ]
257270
258- def __init__ (
259- self , config : schema .ServiceConfig , main_config : schema .MainSectionConfig
260- ) -> None :
271+ def __init__ (self , config : ServiceConfig , main_config : MainSectionConfig ) -> None :
261272 over_version = math .floor (LATEST_API_VERSION ) + 1
262273 if self .API_VERSION >= over_version :
263274 raise ValueError (
@@ -286,9 +297,7 @@ def get_secret(self, key: str, login: str = 'nousername') -> str:
286297 password = getattr (self .config , key )
287298 keyring_service = self .get_keyring_service (self .config )
288299 if not password or password .startswith ("@oracle:" ):
289- password = secrets .get_service_password (
290- keyring_service , login , oracle = password
291- )
300+ password = get_service_password (keyring_service , login , oracle = password )
292301 return password
293302
294303 def get_issue_for_record (
@@ -362,7 +371,7 @@ def issues(self) -> Iterator[T_Issue]:
362371
363372 @staticmethod
364373 @abc .abstractmethod
365- def get_keyring_service (config : schema . ServiceConfig ) -> str :
374+ def get_keyring_service (config : ServiceConfig ) -> str :
366375 """Return the keyring name for this service."""
367376 raise NotImplementedError
368377
@@ -386,5 +395,25 @@ def json_response(response: requests.Response) -> Any:
386395 return response .json ()
387396
388397
398+ @cache
399+ def get_service (service_name : str ) -> type ["Service" ]:
400+ try :
401+ (service ,) = entry_points (group = 'bugwarrior.service' , name = service_name )
402+ except ValueError as e :
403+ if service_name in [
404+ 'activecollab' ,
405+ 'activecollab2' ,
406+ 'megaplan' ,
407+ 'teamlab' ,
408+ 'versionone' ,
409+ ]:
410+ log .warning (f"The { service_name } service has been removed." )
411+ raise ValueError (
412+ f"Configured service '{ service_name } ' not found. "
413+ "Is it installed? Or misspelled?"
414+ ) from e
415+ return service .load ()
416+
417+
389418# NOTE: __all__ determines the stable, public API.
390419__all__ = [Client .__name__ , Issue .__name__ , Service .__name__ ]
0 commit comments