Skip to content

Commit e6b81e8

Browse files
authored
v0.4.0
2 parents 629c2a3 + 53925a2 commit e6b81e8

14 files changed

+247
-151
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
.idea
22
__pycache__
33
Pipfile.lock
4+
tests/
5+
.python-version

Pipfile

-10
This file was deleted.

forecast/client.py

+9-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional
1+
from typing import Any, Dict, Literal, Optional
22

33
import requests
44

@@ -12,7 +12,6 @@ def __init__(self, api_key: str) -> None:
1212
self._session = requests.Session()
1313

1414
# Interface for interacting with people, including creating new people.
15-
# To get an individual person, use `ForecastClient.person()`
1615
self.people = forecast.models.PeopleHelper(self)
1716

1817
# Interface frore interacting with tasks
@@ -32,24 +31,20 @@ def __init__(self, api_key: str) -> None:
3231

3332
def request(self,
3433
path: str,
35-
request_type: Optional[str] = 'GET',
36-
params: Optional[dict] = None,
37-
headers: Optional[dict] = None,
38-
data: Optional[dict] = None) -> dict:
39-
if not isinstance(request_type, str):
40-
raise TypeError('`request_type` should be string.')
34+
request_type: Literal['GET', 'POST', 'PUT', 'DELETE'] = 'GET',
35+
params: Optional[Dict[str, Any]] = None,
36+
headers: Optional[Dict[str, str]] = None,
37+
data: Optional[Dict[str, Any]] = None) -> dict:
4138
request_type = request_type.upper()
42-
valid_types = {'GET', 'POST', 'PUT', 'DELETE'}
43-
if request_type not in valid_types:
44-
raise ValueError(f'`request_type` should be one of: {", ".join(valid_types)}.')
4539

4640
final_headers = {
4741
'x-forecast-api-key': self.api_key,
4842
}
4943
if isinstance(headers, dict):
5044
final_headers.update(headers)
5145

52-
req = requests.Request(request_type, f'https://api.forecast.it/api{path}',
46+
req = requests.Request(request_type,
47+
f'https://api.forecast.it/api{path}',
5348
params=params,
5449
headers=final_headers,
5550
json=data)
@@ -61,6 +56,7 @@ def request(self,
6156
res.raise_for_status()
6257
except requests.exceptions.HTTPError as e:
6358
message = json.get('message')
64-
raise ForecastAPIError(f'Forecast API responded with status {res.status_code}: {message}') from e
59+
raise ForecastAPIError(f'Forecast API responded with status '
60+
f'{res.status_code}: {message}') from e
6561

6662
return json

forecast/const.py

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
'person_id': '/v1/persons/{id}',
1414
'persons': '/v1/persons',
1515
'project_id': '/v1/projects/{id}',
16+
'project_team': '/v1/projects/{id}/team',
1617
'project_company_id': '/v1/projects/company_project_id/{id}',
1718
'projects': '/v1/projects',
1819
'role_id': '/v1/roles/{id}',

forecast/models/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .people import Person
1+
from .people import Person, ProjectTeam
22
from .roles import Role
33
from .labels import Label
44
from .tasks import Task

forecast/models/base.py

+20
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,27 @@
11
import datetime
2+
from typing import TYPE_CHECKING, Any, Dict, Optional
3+
4+
if TYPE_CHECKING:
5+
import forecast
26

37

48
class ForecastBase:
9+
def __init__(self,
10+
_forecast: 'forecast.ForecastClient',
11+
_id: int,
12+
raw: Optional[Dict[str, Any]] = None):
13+
self._forecast = _forecast
14+
self._id = _id
15+
self.raw = raw
16+
self.path = None # Must be set by children
17+
18+
def __getattribute__(self, item):
19+
""" Lazy loads the API response"""
20+
if item == 'raw' and not object.__getattribute__(self, 'raw'):
21+
self.raw = object.__getattribute__(self, '_forecast')\
22+
.request(self.path)
23+
return object.__getattribute__(self, item)
24+
525
@property
626
def id(self) -> int:
727
return self._id

forecast/models/labels.py

+6-12
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,9 @@ def __init__(self,
1212
_forecast: 'forecast.ForecastClient',
1313
_id: int,
1414
raw: Optional[Dict[str, Any]] = None):
15-
self._forecast = _forecast
16-
self._id = _id
17-
self.raw = raw
18-
19-
def __getattribute__(self, item):
20-
# Lazy load the JSON response so that we can create a Label without it
21-
if item == 'raw' and not object.__getattribute__(self, 'raw'):
22-
path = API_PATH['label_id'].format(id=object.__getattribute__(self, '_id'))
23-
self.raw = object.__getattribute__(self, '_forecast').request(path)
24-
return object.__getattribute__(self, item)
15+
super(Label, self).__init__(_forecast, _id, raw)
16+
self.path = API_PATH['label_id'].format(
17+
id=object.__getattribute__(self, '_id'))
2518

2619
@property
2720
def name(self) -> str:
@@ -33,6 +26,7 @@ def color(self) -> str:
3326

3427
def __repr__(self):
3528
if object.__getattribute__(self, 'raw'):
36-
return f'<forecast.Label(id=\'{self.id}\', name=\'{self.name}\')>'
29+
return (f'<forecast.{type(self).__name__}(id=\'{self.id}\', '
30+
f'name=\'{self.name}\')>')
3731
else:
38-
return f'<forecast.Label(id=\'{self.id}\')>'
32+
return f'<forecast.{type(self).__name__}(id=\'{self.id}\')>'

forecast/models/people.py

+51-22
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,20 @@
11
from typing import TYPE_CHECKING, Any, Optional, Dict
22

3+
from .base import ForecastBase
34
from ..const import API_PATH
45

56
if TYPE_CHECKING:
67
import forecast
78

89

9-
class Person(object):
10+
class Person(ForecastBase, object):
1011
def __init__(self,
1112
_forecast: 'forecast.ForecastClient',
1213
_id: int,
1314
raw: Optional[Dict[str, Any]] = None):
14-
self._forecast = _forecast
15-
self._id = _id
16-
self.raw = raw
17-
18-
def __getattribute__(self, item):
19-
# Lazy load the JSON response so that we can create a Person without it
20-
if item == 'raw' and not object.__getattribute__(self, 'raw'):
21-
path = API_PATH['person_id'].format(id=object.__getattribute__(self, '_id'))
22-
self.raw = object.__getattribute__(self, '_forecast').request(path)
23-
return object.__getattribute__(self, item)
24-
25-
@property
26-
def id(self) -> int:
27-
return self._id
15+
super(Person, self).__init__(_forecast, _id, raw)
16+
self.path = API_PATH['person_id'].format(
17+
id=object.__getattribute__(self, '_id'))
2818

2919
@property
3020
def name(self) -> str:
@@ -78,16 +68,55 @@ def start_date(self) -> Optional[str]:
7868
def end_date(self) -> Optional[str]:
7969
return self.raw.get('end_date')
8070

81-
@property
82-
def created_at(self) -> str:
83-
return self.raw['created_at']
71+
def __repr__(self):
72+
if object.__getattribute__(self, 'raw'):
73+
return (f'<forecast.{type(self).__name__}(id=\'{self.id}\', '
74+
f'name=\'{self.name}\')>')
75+
else:
76+
return f'<forecast.{type(self).__name__}(id=\'{self.id}\')>'
77+
78+
79+
class ProjectTeam:
80+
def __init__(self,
81+
_forecast: 'forecast.ForecastClient',
82+
project: 'forecast.models.Project',
83+
raw: Optional[Dict[str, Any]] = None):
84+
self._forecast = _forecast
85+
self.project = project
86+
self.raw = raw
87+
88+
def __len__(self):
89+
return len(self.members)
90+
91+
def __iter__(self):
92+
return iter(self.members)
8493

8594
@property
86-
def updated_at(self) -> str:
87-
return self.raw['updated_at']
95+
def members(self):
96+
return [ProjectTeamMember(self._forecast,
97+
member['person_id'],
98+
self.project,
99+
member['project_role'],
100+
member['project_contact'])
101+
for member in self.raw]
88102

89103
def __repr__(self):
90104
if object.__getattribute__(self, 'raw'):
91-
return f'<forecast.Person(id=\'{self.id}\', name=\'{self.name}\')>'
105+
return (f'<forecast.{type(self).__name__}(project=\'{self.project.id}\', '
106+
f'members=\'{len(self)}\')>')
92107
else:
93-
return f'<forecast.Person(id=\'{self.id}\')>'
108+
return f'<forecast.{type(self).__name__}(project=\'{self.project.id}\')>'
109+
110+
111+
class ProjectTeamMember(Person):
112+
def __init__(self,
113+
_forecast: 'forecast.ForecastClient',
114+
_id: int,
115+
project: 'forecast.models.Project',
116+
_project_role: int,
117+
project_contact: bool):
118+
super(ProjectTeamMember, self).__init__(_forecast, _id)
119+
self.project = project
120+
self._project_role = _project_role
121+
self.project_contact = project_contact
122+

0 commit comments

Comments
 (0)