bloomy-python 0.12.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bloomy/__init__.py +78 -0
- bloomy/client.py +95 -0
- bloomy/configuration.py +137 -0
- bloomy/exceptions.py +27 -0
- bloomy/models.py +371 -0
- bloomy/operations/__init__.py +19 -0
- bloomy/operations/goals.py +270 -0
- bloomy/operations/headlines.py +199 -0
- bloomy/operations/issues.py +187 -0
- bloomy/operations/meetings.py +304 -0
- bloomy/operations/scorecard.py +152 -0
- bloomy/operations/todos.py +229 -0
- bloomy/operations/users.py +154 -0
- bloomy/py.typed +0 -0
- bloomy/utils/__init__.py +1 -0
- bloomy/utils/base_operations.py +43 -0
- bloomy_python-0.12.1.dist-info/METADATA +253 -0
- bloomy_python-0.12.1.dist-info/RECORD +20 -0
- bloomy_python-0.12.1.dist-info/WHEEL +4 -0
- bloomy_python-0.12.1.dist-info/licenses/LICENSE +201 -0
bloomy/__init__.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Bloomy - Python SDK for Bloom Growth API."""
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
|
|
5
|
+
from .client import Client
|
|
6
|
+
from .configuration import Configuration
|
|
7
|
+
from .exceptions import APIError, BloomyError
|
|
8
|
+
from .models import (
|
|
9
|
+
ArchivedGoalInfo,
|
|
10
|
+
CreatedGoalInfo,
|
|
11
|
+
CreatedIssue,
|
|
12
|
+
CurrentWeek,
|
|
13
|
+
DirectReport,
|
|
14
|
+
Goal,
|
|
15
|
+
GoalInfo,
|
|
16
|
+
GoalListResponse,
|
|
17
|
+
Headline,
|
|
18
|
+
HeadlineDetails,
|
|
19
|
+
HeadlineInfo,
|
|
20
|
+
HeadlineListItem,
|
|
21
|
+
Issue,
|
|
22
|
+
IssueDetails,
|
|
23
|
+
IssueListItem,
|
|
24
|
+
Meeting,
|
|
25
|
+
MeetingAttendee,
|
|
26
|
+
MeetingDetails,
|
|
27
|
+
MeetingInfo,
|
|
28
|
+
MeetingListItem,
|
|
29
|
+
OwnerDetails,
|
|
30
|
+
Position,
|
|
31
|
+
ScorecardItem,
|
|
32
|
+
ScorecardMetric,
|
|
33
|
+
ScorecardWeek,
|
|
34
|
+
Todo,
|
|
35
|
+
UserDetails,
|
|
36
|
+
UserListItem,
|
|
37
|
+
UserSearchResult,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
__version__ = importlib.metadata.version("bloomy")
|
|
42
|
+
except importlib.metadata.PackageNotFoundError:
|
|
43
|
+
__version__ = "unknown"
|
|
44
|
+
__all__ = [
|
|
45
|
+
"Client",
|
|
46
|
+
"Configuration",
|
|
47
|
+
"APIError",
|
|
48
|
+
"BloomyError",
|
|
49
|
+
"ArchivedGoalInfo",
|
|
50
|
+
"CreatedGoalInfo",
|
|
51
|
+
"CreatedIssue",
|
|
52
|
+
"CurrentWeek",
|
|
53
|
+
"DirectReport",
|
|
54
|
+
"Goal",
|
|
55
|
+
"GoalInfo",
|
|
56
|
+
"GoalListResponse",
|
|
57
|
+
"Headline",
|
|
58
|
+
"HeadlineDetails",
|
|
59
|
+
"HeadlineInfo",
|
|
60
|
+
"HeadlineListItem",
|
|
61
|
+
"Issue",
|
|
62
|
+
"IssueDetails",
|
|
63
|
+
"IssueListItem",
|
|
64
|
+
"Meeting",
|
|
65
|
+
"MeetingAttendee",
|
|
66
|
+
"MeetingDetails",
|
|
67
|
+
"MeetingInfo",
|
|
68
|
+
"MeetingListItem",
|
|
69
|
+
"OwnerDetails",
|
|
70
|
+
"Position",
|
|
71
|
+
"ScorecardItem",
|
|
72
|
+
"ScorecardMetric",
|
|
73
|
+
"ScorecardWeek",
|
|
74
|
+
"Todo",
|
|
75
|
+
"UserDetails",
|
|
76
|
+
"UserListItem",
|
|
77
|
+
"UserSearchResult",
|
|
78
|
+
]
|
bloomy/client.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Main client for interacting with the Bloom Growth API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .configuration import Configuration
|
|
10
|
+
from .operations.goals import GoalOperations
|
|
11
|
+
from .operations.headlines import HeadlineOperations
|
|
12
|
+
from .operations.issues import IssueOperations
|
|
13
|
+
from .operations.meetings import MeetingOperations
|
|
14
|
+
from .operations.scorecard import ScorecardOperations
|
|
15
|
+
from .operations.todos import TodoOperations
|
|
16
|
+
from .operations.users import UserOperations
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Client:
|
|
23
|
+
"""The Client class is the main entry point for interacting with the Bloomy API.
|
|
24
|
+
|
|
25
|
+
It provides methods for managing Bloom Growth features.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
```python
|
|
29
|
+
from bloomy import Client
|
|
30
|
+
client = Client()
|
|
31
|
+
client.meeting.list()
|
|
32
|
+
client.user.details()
|
|
33
|
+
client.meeting.delete(123)
|
|
34
|
+
client.scorecard.list()
|
|
35
|
+
client.issue.list()
|
|
36
|
+
client.headline.list()
|
|
37
|
+
```
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, api_key: str | None = None) -> None:
|
|
41
|
+
"""Initialize a new Client instance.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
api_key: The API key to use. If not provided, will attempt to
|
|
45
|
+
load from environment variable (BG_API_KEY) or configuration file.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ValueError: If no API key is provided or found in configuration.
|
|
49
|
+
"""
|
|
50
|
+
# Use Configuration class which handles priority:
|
|
51
|
+
# 1. Explicit api_key parameter
|
|
52
|
+
# 2. BG_API_KEY environment variable
|
|
53
|
+
# 3. Configuration file (~/.bloomy/config.yaml)
|
|
54
|
+
self.configuration = Configuration(api_key)
|
|
55
|
+
|
|
56
|
+
if not self.configuration.api_key:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
"No API key provided. Set it explicitly, via BG_API_KEY "
|
|
59
|
+
"environment variable, or in ~/.bloomy/config.yaml configuration file."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self._api_key = self.configuration.api_key
|
|
63
|
+
self._base_url = "https://app.bloomgrowth.com/api/v1"
|
|
64
|
+
|
|
65
|
+
# Initialize HTTP client
|
|
66
|
+
self._client = httpx.Client(
|
|
67
|
+
base_url=self._base_url,
|
|
68
|
+
headers={
|
|
69
|
+
"Accept": "*/*",
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
72
|
+
},
|
|
73
|
+
timeout=30.0,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Initialize operation classes
|
|
77
|
+
self.user = UserOperations(self._client)
|
|
78
|
+
self.todo = TodoOperations(self._client)
|
|
79
|
+
self.meeting = MeetingOperations(self._client)
|
|
80
|
+
self.goal = GoalOperations(self._client)
|
|
81
|
+
self.scorecard = ScorecardOperations(self._client)
|
|
82
|
+
self.issue = IssueOperations(self._client)
|
|
83
|
+
self.headline = HeadlineOperations(self._client)
|
|
84
|
+
|
|
85
|
+
def __enter__(self) -> Client:
|
|
86
|
+
"""Context manager entry."""
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
def __exit__(self, *args: Any) -> None:
|
|
90
|
+
"""Context manager exit - close the HTTP client."""
|
|
91
|
+
self._client.close()
|
|
92
|
+
|
|
93
|
+
def close(self) -> None:
|
|
94
|
+
"""Close the HTTP client connection."""
|
|
95
|
+
self._client.close()
|
bloomy/configuration.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Configuration management for the Bloomy SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
from urllib.parse import urlencode
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
from .exceptions import AuthenticationError, ConfigurationError
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Configuration:
|
|
20
|
+
"""The Configuration class is responsible for managing authentication."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, api_key: str | None = None) -> None:
|
|
23
|
+
"""Initialize a new Configuration instance.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
api_key: Optional API key. If not provided, will attempt to load from
|
|
27
|
+
environment variable or configuration file.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
```python
|
|
31
|
+
config = Bloomy.Configuration(api_key)
|
|
32
|
+
```
|
|
33
|
+
"""
|
|
34
|
+
self.api_key = api_key or os.environ.get("BG_API_KEY") or self._load_api_key()
|
|
35
|
+
|
|
36
|
+
def configure_api_key(
|
|
37
|
+
self, username: str, password: str, store_key: bool = False
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Configure the API key using the provided username and password.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
username: The username for authentication
|
|
43
|
+
password: The password for authentication
|
|
44
|
+
store_key: Whether to store the API key (default: False)
|
|
45
|
+
|
|
46
|
+
Note:
|
|
47
|
+
This method only fetches and stores the API key if it is currently None.
|
|
48
|
+
It saves the key under '~/.bloomy/config.yaml' if 'store_key: True' is
|
|
49
|
+
passed.
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
```python
|
|
53
|
+
config.configure_api_key("user", "pass", store_key=True)
|
|
54
|
+
config.api_key
|
|
55
|
+
# Returns: 'xxxx...'
|
|
56
|
+
```
|
|
57
|
+
"""
|
|
58
|
+
self.api_key = self._fetch_api_key(username, password)
|
|
59
|
+
if store_key:
|
|
60
|
+
self._store_api_key()
|
|
61
|
+
|
|
62
|
+
def _fetch_api_key(self, username: str, password: str) -> str:
|
|
63
|
+
"""Fetch the API key using the provided username and password.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
username: The username for authentication
|
|
67
|
+
password: The password for authentication
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The fetched API key
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
AuthenticationError: If authentication fails
|
|
74
|
+
"""
|
|
75
|
+
with httpx.Client() as client:
|
|
76
|
+
response = client.post(
|
|
77
|
+
"https://app.bloomgrowth.com/Token",
|
|
78
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
79
|
+
content=urlencode(
|
|
80
|
+
{
|
|
81
|
+
"grant_type": "password",
|
|
82
|
+
"userName": username,
|
|
83
|
+
"password": password,
|
|
84
|
+
}
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if not response.is_success:
|
|
89
|
+
raise AuthenticationError(
|
|
90
|
+
f"Failed to fetch API key: {response.status_code} - {response.text}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
data = response.json()
|
|
94
|
+
return data["access_token"]
|
|
95
|
+
|
|
96
|
+
def _store_api_key(self) -> None:
|
|
97
|
+
"""Store the API key in a local configuration file.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
ConfigurationError: If the API key is None
|
|
101
|
+
"""
|
|
102
|
+
if self.api_key is None:
|
|
103
|
+
raise ConfigurationError("API key is None")
|
|
104
|
+
|
|
105
|
+
config_file = self._config_file
|
|
106
|
+
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
|
|
108
|
+
config_data = {"version": 1, "api_key": self.api_key}
|
|
109
|
+
with open(config_file, "w") as f:
|
|
110
|
+
yaml.dump(config_data, f)
|
|
111
|
+
|
|
112
|
+
def _load_api_key(self) -> str | None:
|
|
113
|
+
"""Load the API key from a local configuration file.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The loaded API key or None if the file does not exist
|
|
117
|
+
"""
|
|
118
|
+
config_file = self._config_file
|
|
119
|
+
if not config_file.exists():
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
with open(config_file) as f:
|
|
124
|
+
data = yaml.safe_load(f)
|
|
125
|
+
return data.get("api_key")
|
|
126
|
+
except Exception:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def _config_dir(self) -> Path:
|
|
131
|
+
"""Return the directory path for the configuration file."""
|
|
132
|
+
return Path.home() / ".bloomy"
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def _config_file(self) -> Path:
|
|
136
|
+
"""Return the file path for the configuration file."""
|
|
137
|
+
return self._config_dir / "config.yaml"
|
bloomy/exceptions.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Exceptions for the Bloomy SDK."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BloomyError(Exception):
|
|
5
|
+
"""Base exception for all Bloomy-related errors."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConfigurationError(BloomyError):
|
|
11
|
+
"""Raised when there's an issue with configuration."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthenticationError(BloomyError):
|
|
17
|
+
"""Raised when authentication fails."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class APIError(BloomyError):
|
|
23
|
+
"""Raised when API returns an error response."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, message: str, status_code: int | None = None) -> None:
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
self.status_code = status_code
|
bloomy/models.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""Pydantic models for the Bloomy SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BloomyBaseModel(BaseModel):
|
|
12
|
+
"""Base model with common configuration for all Bloomy models."""
|
|
13
|
+
|
|
14
|
+
model_config = ConfigDict(
|
|
15
|
+
populate_by_name=True,
|
|
16
|
+
use_enum_values=True,
|
|
17
|
+
validate_assignment=True,
|
|
18
|
+
arbitrary_types_allowed=True,
|
|
19
|
+
str_strip_whitespace=True,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DirectReport(BloomyBaseModel):
|
|
24
|
+
"""Model for direct report information."""
|
|
25
|
+
|
|
26
|
+
id: int
|
|
27
|
+
name: str
|
|
28
|
+
image_url: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Position(BloomyBaseModel):
|
|
32
|
+
"""Model for position information."""
|
|
33
|
+
|
|
34
|
+
id: int
|
|
35
|
+
name: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class UserDetails(BloomyBaseModel):
|
|
39
|
+
"""Model for user details."""
|
|
40
|
+
|
|
41
|
+
id: int
|
|
42
|
+
name: str
|
|
43
|
+
image_url: str
|
|
44
|
+
direct_reports: list[DirectReport] | None = None
|
|
45
|
+
positions: list[Position] | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class UserSearchResult(BloomyBaseModel):
|
|
49
|
+
"""Model for user search results."""
|
|
50
|
+
|
|
51
|
+
id: int
|
|
52
|
+
name: str
|
|
53
|
+
description: str
|
|
54
|
+
email: str
|
|
55
|
+
organization_id: int
|
|
56
|
+
image_url: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class UserListItem(BloomyBaseModel):
|
|
60
|
+
"""Model for user list items."""
|
|
61
|
+
|
|
62
|
+
id: int
|
|
63
|
+
name: str
|
|
64
|
+
email: str
|
|
65
|
+
position: str
|
|
66
|
+
image_url: str
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class MeetingAttendee(BloomyBaseModel):
|
|
70
|
+
"""Model for meeting attendee."""
|
|
71
|
+
|
|
72
|
+
user_id: int = Field(alias="UserId")
|
|
73
|
+
name: str = Field(alias="Name")
|
|
74
|
+
image_url: str = Field(alias="ImageUrl")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class MeetingListItem(BloomyBaseModel):
|
|
78
|
+
"""Model for meeting list item (simplified response)."""
|
|
79
|
+
|
|
80
|
+
id: int = Field(alias="Id")
|
|
81
|
+
type: str = Field(alias="Type")
|
|
82
|
+
key: str = Field(alias="Key")
|
|
83
|
+
name: str = Field(alias="Name")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Meeting(BloomyBaseModel):
|
|
87
|
+
"""Model for meeting."""
|
|
88
|
+
|
|
89
|
+
id: int = Field(alias="Id")
|
|
90
|
+
name: str = Field(alias="Name")
|
|
91
|
+
start_date_utc: datetime = Field(alias="StartDateUtc")
|
|
92
|
+
created_date: datetime = Field(alias="CreateDate")
|
|
93
|
+
organization_id: int = Field(alias="OrganizationId")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class MeetingDetails(BloomyBaseModel):
|
|
97
|
+
"""Model for meeting details."""
|
|
98
|
+
|
|
99
|
+
id: int
|
|
100
|
+
name: str
|
|
101
|
+
start_date_utc: datetime | None = None
|
|
102
|
+
created_date: datetime | None = None
|
|
103
|
+
organization_id: int | None = None
|
|
104
|
+
attendees: list[MeetingAttendee] | None = None
|
|
105
|
+
issues: list[Issue] | None = None
|
|
106
|
+
todos: list[Todo] | None = None
|
|
107
|
+
metrics: list[ScorecardMetric] | None = None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class Todo(BloomyBaseModel):
|
|
111
|
+
"""Model for todo."""
|
|
112
|
+
|
|
113
|
+
id: int = Field(alias="Id")
|
|
114
|
+
name: str = Field(alias="Name")
|
|
115
|
+
details_url: str | None = Field(alias="DetailsUrl", default=None)
|
|
116
|
+
due_date: datetime | None = Field(alias="DueDate", default=None)
|
|
117
|
+
complete_date: datetime | None = Field(alias="CompleteTime", default=None)
|
|
118
|
+
create_date: datetime | None = Field(alias="CreateTime", default=None)
|
|
119
|
+
meeting_id: int | None = Field(alias="OriginId", default=None)
|
|
120
|
+
meeting_name: str | None = Field(alias="Origin", default=None)
|
|
121
|
+
complete: bool = Field(alias="Complete", default=False)
|
|
122
|
+
|
|
123
|
+
@field_validator("due_date", "complete_date", "create_date", mode="before")
|
|
124
|
+
@classmethod
|
|
125
|
+
def parse_optional_datetime(cls, v: Any) -> datetime | None:
|
|
126
|
+
"""Parse optional datetime fields."""
|
|
127
|
+
if v is None or v == "":
|
|
128
|
+
return None
|
|
129
|
+
return v
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Issue(BloomyBaseModel):
|
|
133
|
+
"""Model for issue."""
|
|
134
|
+
|
|
135
|
+
id: int = Field(alias="Id")
|
|
136
|
+
name: str = Field(alias="Name")
|
|
137
|
+
details_url: str | None = Field(alias="DetailsUrl", default=None)
|
|
138
|
+
created_date: datetime = Field(alias="CreateDate")
|
|
139
|
+
meeting_id: int = Field(alias="MeetingId")
|
|
140
|
+
meeting_name: str = Field(alias="MeetingName")
|
|
141
|
+
owner_name: str = Field(alias="OwnerName")
|
|
142
|
+
owner_id: int = Field(alias="OwnerId")
|
|
143
|
+
owner_image_url: str = Field(alias="OwnerImageUrl")
|
|
144
|
+
closed_date: datetime | None = Field(alias="ClosedDate", default=None)
|
|
145
|
+
completion_date: datetime | None = Field(alias="CompletionDate", default=None)
|
|
146
|
+
|
|
147
|
+
@field_validator("closed_date", "completion_date", mode="before")
|
|
148
|
+
@classmethod
|
|
149
|
+
def parse_optional_datetime(cls, v: Any) -> datetime | None:
|
|
150
|
+
"""Parse optional datetime fields."""
|
|
151
|
+
if v is None or v == "":
|
|
152
|
+
return None
|
|
153
|
+
return v
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Headline(BloomyBaseModel):
|
|
157
|
+
"""Model for headline."""
|
|
158
|
+
|
|
159
|
+
id: int = Field(alias="Id")
|
|
160
|
+
title: str = Field(alias="Title")
|
|
161
|
+
notes: str = Field(alias="Notes")
|
|
162
|
+
owner_name: str = Field(alias="OwnerName")
|
|
163
|
+
owner_id: int = Field(alias="OwnerId")
|
|
164
|
+
headline_type: str = Field(alias="HeadlineType")
|
|
165
|
+
create_date: datetime = Field(alias="CreateDate")
|
|
166
|
+
meeting_id: int = Field(alias="MeetingId")
|
|
167
|
+
is_archived: bool = Field(alias="IsArchived")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class Goal(BloomyBaseModel):
|
|
171
|
+
"""Model for goal (rock)."""
|
|
172
|
+
|
|
173
|
+
id: int = Field(alias="Id")
|
|
174
|
+
name: str = Field(alias="Name")
|
|
175
|
+
due_date: datetime = Field(alias="DueDate")
|
|
176
|
+
complete_date: datetime | None = Field(alias="CompleteDate", default=None)
|
|
177
|
+
create_date: datetime = Field(alias="CreateDate")
|
|
178
|
+
is_archived: bool = Field(alias="IsArchived", default=False)
|
|
179
|
+
percent_complete: float = Field(alias="PercentComplete", default=0.0)
|
|
180
|
+
accountable_user_id: int = Field(alias="AccountableUserId")
|
|
181
|
+
accountable_user_name: str | None = Field(alias="AccountableUserName", default=None)
|
|
182
|
+
|
|
183
|
+
@field_validator("complete_date", mode="before")
|
|
184
|
+
@classmethod
|
|
185
|
+
def parse_optional_datetime(cls, v: Any) -> datetime | None:
|
|
186
|
+
"""Parse optional datetime fields."""
|
|
187
|
+
if v is None or v == "":
|
|
188
|
+
return None
|
|
189
|
+
return v
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class ScorecardMetric(BloomyBaseModel):
|
|
193
|
+
"""Model for scorecard metric."""
|
|
194
|
+
|
|
195
|
+
id: int = Field(alias="Id")
|
|
196
|
+
title: str = Field(alias="Title")
|
|
197
|
+
target: float | None = Field(alias="Target", default=None)
|
|
198
|
+
unit: str | None = Field(alias="Unit", default=None)
|
|
199
|
+
week_number: int = Field(alias="WeekNumber")
|
|
200
|
+
value: float | None = Field(alias="Value", default=None)
|
|
201
|
+
metric_type: str = Field(alias="MetricType")
|
|
202
|
+
accountable_user_id: int = Field(alias="AccountableUserId")
|
|
203
|
+
accountable_user_name: str | None = Field(alias="AccountableUserName", default=None)
|
|
204
|
+
is_inverse: bool = Field(alias="IsInverse", default=False)
|
|
205
|
+
|
|
206
|
+
@field_validator("target", "value", mode="before")
|
|
207
|
+
@classmethod
|
|
208
|
+
def parse_optional_float(cls, v: Any) -> float | None:
|
|
209
|
+
"""Parse optional float fields."""
|
|
210
|
+
if v is None or v == "":
|
|
211
|
+
return None
|
|
212
|
+
return float(v)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class CurrentWeek(BloomyBaseModel):
|
|
216
|
+
"""Model for current week information."""
|
|
217
|
+
|
|
218
|
+
week_number: int
|
|
219
|
+
start_date: datetime
|
|
220
|
+
end_date: datetime
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class GoalInfo(BloomyBaseModel):
|
|
224
|
+
"""Model for goal information."""
|
|
225
|
+
|
|
226
|
+
id: int
|
|
227
|
+
user_id: int
|
|
228
|
+
user_name: str
|
|
229
|
+
title: str
|
|
230
|
+
created_at: str
|
|
231
|
+
due_date: str | None
|
|
232
|
+
status: str
|
|
233
|
+
meeting_id: int | None = None
|
|
234
|
+
meeting_title: str | None = None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class ArchivedGoalInfo(BloomyBaseModel):
|
|
238
|
+
"""Model for archived goal information."""
|
|
239
|
+
|
|
240
|
+
id: int
|
|
241
|
+
title: str
|
|
242
|
+
created_at: str
|
|
243
|
+
due_date: str | None
|
|
244
|
+
status: str
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class GoalListResponse(BloomyBaseModel):
|
|
248
|
+
"""Model for goal list response with archived goals."""
|
|
249
|
+
|
|
250
|
+
active: list[GoalInfo]
|
|
251
|
+
archived: list[ArchivedGoalInfo]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class CreatedGoalInfo(BloomyBaseModel):
|
|
255
|
+
"""Model for created goal information."""
|
|
256
|
+
|
|
257
|
+
id: int
|
|
258
|
+
user_id: int
|
|
259
|
+
user_name: str
|
|
260
|
+
title: str
|
|
261
|
+
meeting_id: int
|
|
262
|
+
meeting_title: str
|
|
263
|
+
status: str
|
|
264
|
+
created_at: str
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class ScorecardWeek(BloomyBaseModel):
|
|
268
|
+
"""Model for scorecard week details."""
|
|
269
|
+
|
|
270
|
+
id: int
|
|
271
|
+
week_number: int
|
|
272
|
+
week_start: str
|
|
273
|
+
week_end: str
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class ScorecardItem(BloomyBaseModel):
|
|
277
|
+
"""Model for scorecard items."""
|
|
278
|
+
|
|
279
|
+
id: int
|
|
280
|
+
measurable_id: int
|
|
281
|
+
accountable_user_id: int
|
|
282
|
+
title: str
|
|
283
|
+
target: float
|
|
284
|
+
value: float | None = None
|
|
285
|
+
week: str # Changed from int to str to handle "2024-W25" format
|
|
286
|
+
week_id: int
|
|
287
|
+
updated_at: str
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class IssueDetails(BloomyBaseModel):
|
|
291
|
+
"""Model for issue details."""
|
|
292
|
+
|
|
293
|
+
id: int
|
|
294
|
+
title: str
|
|
295
|
+
notes_url: str
|
|
296
|
+
created_at: str
|
|
297
|
+
completed_at: str | None = None
|
|
298
|
+
meeting_id: int
|
|
299
|
+
meeting_title: str
|
|
300
|
+
user_id: int
|
|
301
|
+
user_name: str
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class IssueListItem(BloomyBaseModel):
|
|
305
|
+
"""Model for issue list items."""
|
|
306
|
+
|
|
307
|
+
id: int
|
|
308
|
+
title: str
|
|
309
|
+
notes_url: str
|
|
310
|
+
created_at: str
|
|
311
|
+
meeting_id: int
|
|
312
|
+
meeting_title: str
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class CreatedIssue(BloomyBaseModel):
|
|
316
|
+
"""Model for created issue response."""
|
|
317
|
+
|
|
318
|
+
id: int
|
|
319
|
+
meeting_id: int
|
|
320
|
+
meeting_title: str
|
|
321
|
+
title: str
|
|
322
|
+
user_id: int
|
|
323
|
+
notes_url: str
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class OwnerDetails(BloomyBaseModel):
|
|
327
|
+
"""Model for owner details."""
|
|
328
|
+
|
|
329
|
+
id: int
|
|
330
|
+
name: str | None = None
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class MeetingInfo(BloomyBaseModel):
|
|
334
|
+
"""Model for meeting information."""
|
|
335
|
+
|
|
336
|
+
id: int
|
|
337
|
+
title: str | None = None
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class HeadlineInfo(BloomyBaseModel):
|
|
341
|
+
"""Model for headline information."""
|
|
342
|
+
|
|
343
|
+
id: int
|
|
344
|
+
title: str
|
|
345
|
+
notes_url: str
|
|
346
|
+
owner_details: OwnerDetails
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class HeadlineDetails(BloomyBaseModel):
|
|
350
|
+
"""Model for detailed headline information."""
|
|
351
|
+
|
|
352
|
+
id: int
|
|
353
|
+
title: str
|
|
354
|
+
notes_url: str
|
|
355
|
+
meeting_details: MeetingInfo
|
|
356
|
+
owner_details: OwnerDetails
|
|
357
|
+
archived: bool
|
|
358
|
+
created_at: str
|
|
359
|
+
closed_at: str | None = None
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class HeadlineListItem(BloomyBaseModel):
|
|
363
|
+
"""Model for headline list items."""
|
|
364
|
+
|
|
365
|
+
id: int
|
|
366
|
+
title: str
|
|
367
|
+
meeting_details: MeetingInfo
|
|
368
|
+
owner_details: OwnerDetails
|
|
369
|
+
archived: bool
|
|
370
|
+
created_at: str
|
|
371
|
+
closed_at: str | None = None
|