Skip to content

Commit beda58c

Browse files
Release version 1.1.0 (#2)
* chore: Update repository URLs to getlatedev/late-python-sdk * fix: Resolve lint errors and update MCP test imports * fix: Resolve mypy errors and add params to HTTP methods * ci: Improve release workflow with better visibility and PyPI check - Add release summary in GitHub Actions - Check if version exists on PyPI before publishing - Show clear notices for skip/release decisions - Bump version to 1.0.1 * ci: Add release preview comment on PRs to main Shows version info and release status before merging: - Version from pyproject.toml - Whether git tag exists - Whether version exists on PyPI - Clear indication if release will happen or be skipped * chore: trigger workflow re-run * fix: Add explicit permissions for checkout in private repo * fix: Add explicit token and permissions to all workflows for private repo * feat: Add typed responses with Pydantic models (v1.1.0) - All resource methods now return typed Pydantic models instead of dicts - Generate proper Enum classes instead of Literal types - Add response models: PostsListResponse, ProfileGetResponse, etc. - Add upload module with direct and Vercel Blob support - Update tests to use attribute access syntax - Sync version to 1.1.0 across pyproject.toml and __init__.py * feat(mcp): Add is_draft parameter and centralized tool definitions - Add is_draft parameter to posts_create and posts_cross_post - Create tool_definitions.py as single source of truth for MCP params - Add script to generate MDX docs from definitions * fix: Move Callable imports to TYPE_CHECKING block and fix trailing whitespace * fix: Fix mypy errors and format code * feat(ai): Add model property to OpenAI provider --------- Co-authored-by: Carlos Martínez <carlimvg02@gmail.com>
1 parent 17466b4 commit beda58c

39 files changed

Lines changed: 3694 additions & 381 deletions

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "late-sdk"
3-
version = "1.0.1"
3+
version = "1.1.0"
44
description = "Python SDK for Late API - Social Media Scheduling"
55
readme = "README.md"
66
requires-python = ">=3.10"

scripts/generate_mcp_docs.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Generate MCP documentation from tool definitions.
4+
5+
Usage:
6+
python scripts/generate_mcp_docs.py
7+
8+
This script generates MDX documentation from the centralized tool definitions
9+
in src/late/mcp/tool_definitions.py
10+
"""
11+
12+
import sys
13+
from pathlib import Path
14+
15+
# Add src to path for imports
16+
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
17+
18+
from late.mcp.tool_definitions import generate_mdx_docs, TOOL_DEFINITIONS
19+
20+
21+
def main():
22+
"""Generate and print MDX documentation."""
23+
print("=" * 60)
24+
print("MCP Tool Documentation (generated from tool_definitions.py)")
25+
print("=" * 60)
26+
print()
27+
print(generate_mdx_docs())
28+
print()
29+
print("=" * 60)
30+
print("Copy the above into claude-mcp.mdx under '## Tool Reference'")
31+
print("=" * 60)
32+
33+
34+
if __name__ == "__main__":
35+
main()

scripts/generate_models.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def main() -> int:
3434

3535
# Create output directory
3636
output_dir.mkdir(parents=True, exist_ok=True)
37+
output_file = output_dir / "models.py"
3738

3839
# Run datamodel-code-generator
3940
cmd = [
@@ -43,7 +44,7 @@ def main() -> int:
4344
"--input",
4445
str(openapi_spec),
4546
"--output",
46-
str(output_dir),
47+
str(output_file),
4748
"--output-model-type",
4849
"pydantic_v2.BaseModel",
4950
"--input-file-type",
@@ -55,8 +56,6 @@ def main() -> int:
5556
"--field-constraints",
5657
"--use-field-description",
5758
"--capitalise-enum-members",
58-
"--enum-field-as-literal",
59-
"all",
6059
"--use-default-kwarg",
6160
"--collapse-root-models",
6261
"--use-union-operator",

src/late/__init__.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,42 @@
1616
LateValidationError,
1717
)
1818
from .client.late_client import Late
19+
from .enums import (
20+
CaptionTone,
21+
DayOfWeek,
22+
FacebookContentType,
23+
GoogleBusinessCTAType,
24+
InstagramContentType,
25+
MediaType,
26+
Platform,
27+
PostStatus,
28+
TikTokCommercialContentType,
29+
TikTokMediaType,
30+
TikTokPrivacyLevel,
31+
Visibility,
32+
)
1933

20-
__version__ = "1.0.0"
34+
__version__ = "1.1.0"
2135

2236
__all__ = [
37+
# Client
2338
"Late",
39+
# Enums - Core
40+
"Platform",
41+
"PostStatus",
42+
"MediaType",
43+
"Visibility",
44+
# Enums - Platform-specific
45+
"InstagramContentType",
46+
"FacebookContentType",
47+
"TikTokPrivacyLevel",
48+
"TikTokCommercialContentType",
49+
"TikTokMediaType",
50+
"GoogleBusinessCTAType",
51+
# Enums - Tools & Queue
52+
"CaptionTone",
53+
"DayOfWeek",
54+
# Exceptions
2455
"LateAPIError",
2556
"LateAuthenticationError",
2657
"LateConnectionError",

src/late/ai/content_generator.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from typing import TYPE_CHECKING, Any
88

9+
from late.enums import CaptionTone, Platform
10+
911
from .protocols import (
1012
AIProvider,
1113
GenerateRequest,
@@ -25,15 +27,16 @@ class ContentGenerator:
2527
2628
Example:
2729
>>> from late.ai import ContentGenerator, GenerateRequest
30+
>>> from late import Platform, CaptionTone
2831
>>>
2932
>>> # Using OpenAI
3033
>>> generator = ContentGenerator(provider="openai", api_key="sk-...")
3134
>>>
3235
>>> response = generator.generate(
3336
... GenerateRequest(
3437
... prompt="Write a tweet about Python",
35-
... platform="twitter",
36-
... tone="professional",
38+
... platform=Platform.TWITTER,
39+
... tone=CaptionTone.PROFESSIONAL,
3740
... )
3841
... )
3942
>>> print(response.text)
@@ -94,9 +97,7 @@ async def agenerate(self, request: GenerateRequest) -> GenerateResponse:
9497
"""Generate content asynchronously."""
9598
return await self._provider.agenerate(request)
9699

97-
async def agenerate_stream(
98-
self, request: GenerateRequest
99-
) -> AsyncIterator[str]:
100+
async def agenerate_stream(self, request: GenerateRequest) -> AsyncIterator[str]:
100101
"""Generate content as a stream."""
101102
if not isinstance(self._provider, StreamingAIProvider):
102103
raise NotImplementedError(
@@ -109,9 +110,9 @@ async def agenerate_stream(
109110
def generate_post(
110111
self,
111112
topic: str,
112-
platform: str,
113+
platform: Platform | str,
113114
*,
114-
tone: str = "professional",
115+
tone: CaptionTone | str = CaptionTone.PROFESSIONAL,
115116
language: str = "en",
116117
**kwargs: Any,
117118
) -> str:
@@ -139,9 +140,9 @@ def generate_post(
139140
async def agenerate_post(
140141
self,
141142
topic: str,
142-
platform: str,
143+
platform: Platform | str,
143144
*,
144-
tone: str = "professional",
145+
tone: CaptionTone | str = CaptionTone.PROFESSIONAL,
145146
language: str = "en",
146147
**kwargs: Any,
147148
) -> str:

src/late/ai/protocols.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
if TYPE_CHECKING:
1313
from collections.abc import AsyncIterator
1414

15+
from late.enums import CaptionTone, Platform
16+
1517

1618
@dataclass
1719
class GenerateRequest:
@@ -21,8 +23,8 @@ class GenerateRequest:
2123
system: str | None = None
2224
max_tokens: int = 500
2325
temperature: float = 0.7
24-
platform: str | None = None # e.g., "twitter", "linkedin"
25-
tone: str | None = None # e.g., "professional", "casual"
26+
platform: Platform | str | None = None
27+
tone: CaptionTone | str | None = None
2628
language: str = "en"
2729
context: dict[str, Any] = field(default_factory=dict)
2830

@@ -68,8 +70,6 @@ class StreamingAIProvider(Protocol):
6870
"""Protocol for streaming content generation."""
6971

7072
@abstractmethod
71-
async def agenerate_stream(
72-
self, request: GenerateRequest
73-
) -> AsyncIterator[str]:
73+
async def agenerate_stream(self, request: GenerateRequest) -> AsyncIterator[str]:
7474
"""Generate content as a stream."""
7575
...

src/late/ai/providers/openai.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import os
88
from typing import TYPE_CHECKING, Any
99

10+
from late.enums import Platform
11+
1012
from ..protocols import GenerateRequest, GenerateResponse
1113

1214
if TYPE_CHECKING:
@@ -55,8 +57,14 @@ def __init__(
5557
def name(self) -> str:
5658
return "openai"
5759

60+
@property
61+
def model(self) -> str:
62+
"""Current model being used."""
63+
return self._model
64+
5865
@property
5966
def default_model(self) -> str:
67+
"""Default model if none specified."""
6068
return "gpt-4o-mini"
6169

6270
def _build_messages(self, request: GenerateRequest) -> list[dict[str, str]]:
@@ -76,12 +84,12 @@ def _build_system_prompt(self, request: GenerateRequest) -> str:
7684
parts = ["You are an expert social media content creator."]
7785

7886
if request.platform:
79-
platform_guides = {
80-
"twitter": "Keep it under 280 characters. Be concise and engaging.",
81-
"linkedin": "Be professional and insightful. Use paragraphs.",
82-
"instagram": "Be visual and use emojis. Include hashtag suggestions.",
83-
"tiktok": "Be trendy and use Gen-Z language. Keep it fun.",
84-
"facebook": "Be conversational and engaging.",
87+
platform_guides: dict[Platform | str, str] = {
88+
Platform.TWITTER: "Keep it under 280 characters. Be concise and engaging.",
89+
Platform.LINKEDIN: "Be professional and insightful. Use paragraphs.",
90+
Platform.INSTAGRAM: "Be visual and use emojis. Include hashtag suggestions.",
91+
Platform.TIKTOK: "Be trendy and use Gen-Z language. Keep it fun.",
92+
Platform.FACEBOOK: "Be conversational and engaging.",
8593
}
8694
guide = platform_guides.get(request.platform, "")
8795
parts.append(f"Writing for {request.platform}. {guide}")
@@ -140,9 +148,7 @@ async def agenerate(self, request: GenerateRequest) -> GenerateResponse:
140148
finish_reason=choice.finish_reason,
141149
)
142150

143-
async def agenerate_stream(
144-
self, request: GenerateRequest
145-
) -> AsyncIterator[str]:
151+
async def agenerate_stream(self, request: GenerateRequest) -> AsyncIterator[str]:
146152
"""Generate content as a stream."""
147153
stream = await self._async_client.chat.completions.create(
148154
model=self._model,

src/late/client/base.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,14 @@ def _post(
206206
headers=headers,
207207
timeout=self.timeout,
208208
) as client:
209-
return self._request_with_retry(client, "POST", path, files=files, params=params)
209+
return self._request_with_retry(
210+
client, "POST", path, files=files, params=params
211+
)
210212

211213
with self._sync_client() as client:
212-
return self._request_with_retry(client, "POST", path, json=data, params=params)
214+
return self._request_with_retry(
215+
client, "POST", path, json=data, params=params
216+
)
213217

214218
def _put(
215219
self,
@@ -312,10 +316,14 @@ async def _apost(
312316
headers=headers,
313317
timeout=self.timeout,
314318
) as client:
315-
return await self._arequest_with_retry(client, "POST", path, files=files, params=params)
319+
return await self._arequest_with_retry(
320+
client, "POST", path, files=files, params=params
321+
)
316322

317323
async with self._async_client() as client:
318-
return await self._arequest_with_retry(client, "POST", path, json=data, params=params)
324+
return await self._arequest_with_retry(
325+
client, "POST", path, json=data, params=params
326+
)
319327

320328
async def _aput(
321329
self,
@@ -333,4 +341,6 @@ async def _adelete(
333341
) -> dict[str, Any]:
334342
"""Make an async DELETE request."""
335343
async with self._async_client() as client:
336-
return await self._arequest_with_retry(client, "DELETE", path, params=params)
344+
return await self._arequest_with_retry(
345+
client, "DELETE", path, params=params
346+
)

src/late/client/late_client.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class Late(BaseClient):
2222
Late API client for scheduling social media posts.
2323
2424
Example:
25-
>>> from late import Late
25+
>>> from late import Late, Platform
2626
>>>
2727
>>> # Initialize client
2828
>>> client = Late(api_key="your_api_key")
@@ -33,7 +33,7 @@ class Late(BaseClient):
3333
>>> # Create a post
3434
>>> post = client.posts.create(
3535
... content="Hello world!",
36-
... platforms=[{"platform": "twitter", "accountId": "..."}],
36+
... platforms=[{"platform": Platform.TWITTER, "accountId": "..."}],
3737
... scheduled_for="2024-12-25T10:00:00Z",
3838
... )
3939
>>>
@@ -59,7 +59,9 @@ def __init__(
5959
timeout: Request timeout in seconds
6060
max_retries: Maximum retries for failed requests
6161
"""
62-
super().__init__(api_key, base_url=base_url, timeout=timeout, max_retries=max_retries)
62+
super().__init__(
63+
api_key, base_url=base_url, timeout=timeout, max_retries=max_retries
64+
)
6365

6466
# Initialize resources
6567
self.posts = PostsResource(self)

0 commit comments

Comments
 (0)