og-pilot 0.1.0__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.
- og_pilot/__init__.py +136 -0
- og_pilot/client.py +191 -0
- og_pilot/config.py +31 -0
- og_pilot/django/__init__.py +21 -0
- og_pilot/django/apps.py +37 -0
- og_pilot/django/management/__init__.py +1 -0
- og_pilot/django/management/commands/__init__.py +1 -0
- og_pilot/django/management/commands/og_pilot_check.py +66 -0
- og_pilot/django/templates/og_pilot/meta_tags.html +12 -0
- og_pilot/django/templatetags/__init__.py +1 -0
- og_pilot/django/templatetags/og_pilot_tags.py +109 -0
- og_pilot/exceptions.py +25 -0
- og_pilot/jwt_encoder.py +24 -0
- og_pilot-0.1.0.dist-info/METADATA +502 -0
- og_pilot-0.1.0.dist-info/RECORD +17 -0
- og_pilot-0.1.0.dist-info/WHEEL +4 -0
- og_pilot-0.1.0.dist-info/licenses/LICENSE +21 -0
og_pilot/__init__.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OG Pilot Python SDK
|
|
3
|
+
|
|
4
|
+
A Python client for generating OG Pilot Open Graph images via signed JWTs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from og_pilot.client import Client
|
|
8
|
+
from og_pilot.config import Configuration
|
|
9
|
+
from og_pilot.exceptions import ConfigurationError, OgPilotError, RequestError
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Client",
|
|
14
|
+
"Configuration",
|
|
15
|
+
"OgPilotError",
|
|
16
|
+
"ConfigurationError",
|
|
17
|
+
"RequestError",
|
|
18
|
+
"configure",
|
|
19
|
+
"reset_config",
|
|
20
|
+
"get_config",
|
|
21
|
+
"client",
|
|
22
|
+
"create_client",
|
|
23
|
+
"create_image",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
# Global configuration instance
|
|
27
|
+
_config: Configuration | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_config() -> Configuration:
|
|
31
|
+
"""Get the global configuration, creating it if necessary."""
|
|
32
|
+
global _config
|
|
33
|
+
if _config is None:
|
|
34
|
+
_config = Configuration()
|
|
35
|
+
return _config
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def configure(**kwargs) -> Configuration:
|
|
39
|
+
"""
|
|
40
|
+
Configure the global OG Pilot client.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
api_key: Your OG Pilot API key
|
|
44
|
+
domain: Your domain registered with OG Pilot
|
|
45
|
+
base_url: OG Pilot API base URL (default: https://ogpilot.com)
|
|
46
|
+
open_timeout: Connection timeout in seconds (default: 5)
|
|
47
|
+
read_timeout: Read timeout in seconds (default: 10)
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
The updated Configuration instance
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> import og_pilot
|
|
54
|
+
>>> og_pilot.configure(
|
|
55
|
+
... api_key="your-api-key",
|
|
56
|
+
... domain="example.com"
|
|
57
|
+
... )
|
|
58
|
+
"""
|
|
59
|
+
global _config
|
|
60
|
+
if _config is None:
|
|
61
|
+
_config = Configuration(**kwargs)
|
|
62
|
+
else:
|
|
63
|
+
for key, value in kwargs.items():
|
|
64
|
+
if hasattr(_config, key):
|
|
65
|
+
setattr(_config, key, value)
|
|
66
|
+
return _config
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def reset_config() -> None:
|
|
70
|
+
"""Reset the global configuration to defaults."""
|
|
71
|
+
global _config
|
|
72
|
+
_config = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def client() -> Client:
|
|
76
|
+
"""Get a client instance using the global configuration."""
|
|
77
|
+
return Client(get_config())
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def create_client(**kwargs) -> Client:
|
|
81
|
+
"""
|
|
82
|
+
Create a new client with custom configuration.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
api_key: Your OG Pilot API key
|
|
86
|
+
domain: Your domain registered with OG Pilot
|
|
87
|
+
base_url: OG Pilot API base URL
|
|
88
|
+
open_timeout: Connection timeout in seconds
|
|
89
|
+
read_timeout: Read timeout in seconds
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
A new Client instance
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
>>> from og_pilot import create_client
|
|
96
|
+
>>> client = create_client(
|
|
97
|
+
... api_key="your-api-key",
|
|
98
|
+
... domain="example.com"
|
|
99
|
+
... )
|
|
100
|
+
"""
|
|
101
|
+
config = Configuration(**kwargs)
|
|
102
|
+
return Client(config)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def create_image(
|
|
106
|
+
params: dict | None = None,
|
|
107
|
+
*,
|
|
108
|
+
json: bool = False,
|
|
109
|
+
iat: int | float | None = None,
|
|
110
|
+
headers: dict[str, str] | None = None,
|
|
111
|
+
**kwargs,
|
|
112
|
+
) -> str | dict:
|
|
113
|
+
"""
|
|
114
|
+
Generate an OG Pilot image URL using the global configuration.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
params: Dictionary of template parameters
|
|
118
|
+
json: If True, return JSON metadata instead of URL
|
|
119
|
+
iat: Issue time for cache busting (Unix timestamp or datetime)
|
|
120
|
+
headers: Additional HTTP headers
|
|
121
|
+
**kwargs: Additional template parameters (merged with params)
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Image URL string or JSON metadata dict if json=True
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
>>> import og_pilot
|
|
128
|
+
>>> og_pilot.configure(api_key="...", domain="example.com")
|
|
129
|
+
>>> url = og_pilot.create_image(
|
|
130
|
+
... template="blog_post",
|
|
131
|
+
... title="My Blog Post",
|
|
132
|
+
... description="A great article"
|
|
133
|
+
... )
|
|
134
|
+
"""
|
|
135
|
+
merged_params = {**(params or {}), **kwargs}
|
|
136
|
+
return client().create_image(merged_params, json=json, iat=iat, headers=headers)
|
og_pilot/client.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OG Pilot Client
|
|
3
|
+
|
|
4
|
+
HTTP client for the OG Pilot API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
from urllib.parse import urlencode, urljoin
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
from og_pilot import jwt_encoder
|
|
17
|
+
from og_pilot.exceptions import ConfigurationError, RequestError
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from og_pilot.config import Configuration
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
ENDPOINT_PATH = "/api/v1/images"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Client:
|
|
27
|
+
"""
|
|
28
|
+
OG Pilot API client.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
>>> from og_pilot import Client, Configuration
|
|
32
|
+
>>> config = Configuration(api_key="...", domain="example.com")
|
|
33
|
+
>>> client = Client(config)
|
|
34
|
+
>>> url = client.create_image({"template": "default", "title": "Hello"})
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, config: Configuration):
|
|
38
|
+
"""
|
|
39
|
+
Initialize the client with configuration.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
config: Configuration instance
|
|
43
|
+
"""
|
|
44
|
+
self.config = config
|
|
45
|
+
|
|
46
|
+
def create_image(
|
|
47
|
+
self,
|
|
48
|
+
params: dict | None = None,
|
|
49
|
+
*,
|
|
50
|
+
json_response: bool = False,
|
|
51
|
+
iat: int | float | datetime | None = None,
|
|
52
|
+
headers: dict[str, str] | None = None,
|
|
53
|
+
) -> str | dict:
|
|
54
|
+
"""
|
|
55
|
+
Generate an OG Pilot image URL or fetch JSON metadata.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
params: Dictionary of template parameters (must include 'title')
|
|
59
|
+
json_response: If True, return JSON metadata instead of URL
|
|
60
|
+
iat: Issue time for cache busting. Can be Unix timestamp (int/float)
|
|
61
|
+
or datetime object. If omitted, image is cached indefinitely.
|
|
62
|
+
headers: Additional HTTP headers to send with the request
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Image URL string, or JSON metadata dict if json_response=True
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ConfigurationError: If API key or domain is missing
|
|
69
|
+
RequestError: If the API request fails
|
|
70
|
+
ValueError: If required parameters are missing
|
|
71
|
+
"""
|
|
72
|
+
url = self._build_url(params or {}, iat)
|
|
73
|
+
response = self._request(url, json_response=json_response, headers=headers or {})
|
|
74
|
+
|
|
75
|
+
if json_response:
|
|
76
|
+
return json.loads(response.text)
|
|
77
|
+
|
|
78
|
+
# Return the redirect location or the final URL
|
|
79
|
+
return response.headers.get("Location") or response.url or str(url)
|
|
80
|
+
|
|
81
|
+
def _request(
|
|
82
|
+
self,
|
|
83
|
+
url: str,
|
|
84
|
+
*,
|
|
85
|
+
json_response: bool,
|
|
86
|
+
headers: dict[str, str],
|
|
87
|
+
) -> requests.Response:
|
|
88
|
+
"""Make an HTTP request to the OG Pilot API."""
|
|
89
|
+
request_headers = {}
|
|
90
|
+
if json_response:
|
|
91
|
+
request_headers["Accept"] = "application/json"
|
|
92
|
+
request_headers.update(headers)
|
|
93
|
+
|
|
94
|
+
timeout = (self.config.open_timeout, self.config.read_timeout)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
response = requests.get(
|
|
98
|
+
url,
|
|
99
|
+
headers=request_headers,
|
|
100
|
+
timeout=timeout,
|
|
101
|
+
allow_redirects=False,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if response.status_code >= 400:
|
|
105
|
+
raise RequestError(
|
|
106
|
+
f"OG Pilot request failed with status {response.status_code}: {response.text}",
|
|
107
|
+
status_code=response.status_code,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return response
|
|
111
|
+
|
|
112
|
+
except requests.exceptions.SSLError as e:
|
|
113
|
+
raise RequestError(f"OG Pilot request failed with SSL error: {e}")
|
|
114
|
+
except requests.exceptions.ConnectTimeout as e:
|
|
115
|
+
raise RequestError(f"OG Pilot request timed out during connection: {e}")
|
|
116
|
+
except requests.exceptions.ReadTimeout as e:
|
|
117
|
+
raise RequestError(f"OG Pilot request timed out during read: {e}")
|
|
118
|
+
except requests.exceptions.RequestException as e:
|
|
119
|
+
raise RequestError(f"OG Pilot request failed: {e}")
|
|
120
|
+
|
|
121
|
+
def _build_url(self, params: dict, iat: int | float | datetime | None) -> str:
|
|
122
|
+
"""Build the signed URL for the image request."""
|
|
123
|
+
payload = self._build_payload(params, iat)
|
|
124
|
+
token = jwt_encoder.encode(payload, self._api_key)
|
|
125
|
+
base_url = urljoin(self.config.base_url, ENDPOINT_PATH)
|
|
126
|
+
return f"{base_url}?{urlencode({'token': token})}"
|
|
127
|
+
|
|
128
|
+
def _build_payload(self, params: dict, iat: int | float | datetime | None) -> dict:
|
|
129
|
+
"""Build the JWT payload with required claims."""
|
|
130
|
+
payload = dict(params)
|
|
131
|
+
|
|
132
|
+
if iat is not None:
|
|
133
|
+
payload["iat"] = _normalize_iat(iat)
|
|
134
|
+
|
|
135
|
+
if "iss" not in payload or not payload["iss"]:
|
|
136
|
+
payload["iss"] = self._domain
|
|
137
|
+
|
|
138
|
+
if "sub" not in payload or not payload["sub"]:
|
|
139
|
+
payload["sub"] = self._api_key_prefix
|
|
140
|
+
|
|
141
|
+
self._validate_payload(payload)
|
|
142
|
+
return payload
|
|
143
|
+
|
|
144
|
+
def _validate_payload(self, payload: dict) -> None:
|
|
145
|
+
"""Validate required payload fields."""
|
|
146
|
+
if not payload.get("iss"):
|
|
147
|
+
raise ConfigurationError("OG Pilot domain is missing")
|
|
148
|
+
|
|
149
|
+
if not payload.get("sub"):
|
|
150
|
+
raise ConfigurationError("OG Pilot API key prefix is missing")
|
|
151
|
+
|
|
152
|
+
if not payload.get("title"):
|
|
153
|
+
raise ValueError("OG Pilot title is required")
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def _api_key(self) -> str:
|
|
157
|
+
"""Get the API key, raising an error if not configured."""
|
|
158
|
+
if self.config.api_key:
|
|
159
|
+
return self.config.api_key
|
|
160
|
+
raise ConfigurationError("OG Pilot API key is missing")
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def _domain(self) -> str:
|
|
164
|
+
"""Get the domain, raising an error if not configured."""
|
|
165
|
+
if self.config.domain:
|
|
166
|
+
return self.config.domain
|
|
167
|
+
raise ConfigurationError("OG Pilot domain is missing")
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def _api_key_prefix(self) -> str:
|
|
171
|
+
"""Get the first 8 characters of the API key."""
|
|
172
|
+
return self._api_key[:8]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _normalize_iat(iat: int | float | datetime) -> int:
|
|
176
|
+
"""
|
|
177
|
+
Normalize the iat (issued at) value to Unix timestamp seconds.
|
|
178
|
+
|
|
179
|
+
Handles:
|
|
180
|
+
- datetime objects
|
|
181
|
+
- Unix timestamps in milliseconds (> 100000000000)
|
|
182
|
+
- Unix timestamps in seconds
|
|
183
|
+
"""
|
|
184
|
+
if isinstance(iat, datetime):
|
|
185
|
+
return int(iat.timestamp())
|
|
186
|
+
|
|
187
|
+
# If it looks like milliseconds, convert to seconds
|
|
188
|
+
if iat > 100_000_000_000:
|
|
189
|
+
return int(iat / 1000)
|
|
190
|
+
|
|
191
|
+
return int(iat)
|
og_pilot/config.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OG Pilot Configuration
|
|
3
|
+
|
|
4
|
+
Configuration management for the OG Pilot SDK.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
DEFAULT_BASE_URL = "https://ogpilot.com"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Configuration:
|
|
16
|
+
"""
|
|
17
|
+
Configuration for the OG Pilot client.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
api_key: Your OG Pilot API key. Defaults to OG_PILOT_API_KEY env var.
|
|
21
|
+
domain: Your domain registered with OG Pilot. Defaults to OG_PILOT_DOMAIN env var.
|
|
22
|
+
base_url: OG Pilot API base URL. Defaults to https://ogpilot.com.
|
|
23
|
+
open_timeout: Connection timeout in seconds. Defaults to 5.
|
|
24
|
+
read_timeout: Read timeout in seconds. Defaults to 10.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
api_key: str | None = field(default_factory=lambda: os.environ.get("OG_PILOT_API_KEY"))
|
|
28
|
+
domain: str | None = field(default_factory=lambda: os.environ.get("OG_PILOT_DOMAIN"))
|
|
29
|
+
base_url: str = DEFAULT_BASE_URL
|
|
30
|
+
open_timeout: float = 5.0
|
|
31
|
+
read_timeout: float = 10.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OG Pilot Django Integration
|
|
3
|
+
|
|
4
|
+
Django app for easy OG Pilot integration.
|
|
5
|
+
|
|
6
|
+
Add 'og_pilot.django' to your INSTALLED_APPS to use template tags and
|
|
7
|
+
management commands.
|
|
8
|
+
|
|
9
|
+
Example settings.py:
|
|
10
|
+
INSTALLED_APPS = [
|
|
11
|
+
...
|
|
12
|
+
'og_pilot.django',
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
OG_PILOT = {
|
|
16
|
+
'API_KEY': 'your-api-key', # or use OG_PILOT_API_KEY env var
|
|
17
|
+
'DOMAIN': 'example.com', # or use OG_PILOT_DOMAIN env var
|
|
18
|
+
}
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
default_app_config = "og_pilot.django.apps.OgPilotConfig"
|
og_pilot/django/apps.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OG Pilot Django App Configuration
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.apps import AppConfig
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OgPilotConfig(AppConfig):
|
|
10
|
+
"""Django app configuration for OG Pilot."""
|
|
11
|
+
|
|
12
|
+
name = "og_pilot.django"
|
|
13
|
+
verbose_name = "OG Pilot"
|
|
14
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
15
|
+
|
|
16
|
+
def ready(self):
|
|
17
|
+
"""Configure OG Pilot from Django settings when the app is ready."""
|
|
18
|
+
import og_pilot
|
|
19
|
+
|
|
20
|
+
# Get OG_PILOT settings dict, defaulting to empty dict
|
|
21
|
+
og_pilot_settings = getattr(settings, "OG_PILOT", {})
|
|
22
|
+
|
|
23
|
+
config_mapping = {
|
|
24
|
+
"API_KEY": "api_key",
|
|
25
|
+
"DOMAIN": "domain",
|
|
26
|
+
"BASE_URL": "base_url",
|
|
27
|
+
"OPEN_TIMEOUT": "open_timeout",
|
|
28
|
+
"READ_TIMEOUT": "read_timeout",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
config_kwargs = {}
|
|
32
|
+
for settings_key, config_key in config_mapping.items():
|
|
33
|
+
if settings_key in og_pilot_settings:
|
|
34
|
+
config_kwargs[config_key] = og_pilot_settings[settings_key]
|
|
35
|
+
|
|
36
|
+
if config_kwargs:
|
|
37
|
+
og_pilot.configure(**config_kwargs)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Django management commands
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# OG Pilot management commands
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OG Pilot Configuration Check Command
|
|
3
|
+
|
|
4
|
+
Management command to verify OG Pilot configuration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from django.core.management.base import BaseCommand
|
|
8
|
+
|
|
9
|
+
import og_pilot
|
|
10
|
+
from og_pilot.exceptions import ConfigurationError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Command(BaseCommand):
|
|
14
|
+
"""Check OG Pilot configuration and test connectivity."""
|
|
15
|
+
|
|
16
|
+
help = "Check OG Pilot configuration and optionally test with a sample request"
|
|
17
|
+
|
|
18
|
+
def add_arguments(self, parser):
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"--test",
|
|
21
|
+
action="store_true",
|
|
22
|
+
help="Send a test request to verify API connectivity",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def handle(self, *args, **options):
|
|
26
|
+
self.stdout.write("Checking OG Pilot configuration...\n")
|
|
27
|
+
|
|
28
|
+
config = og_pilot.get_config()
|
|
29
|
+
|
|
30
|
+
# Check API key
|
|
31
|
+
if config.api_key:
|
|
32
|
+
masked_key = config.api_key[:4] + "*" * (len(config.api_key) - 8) + config.api_key[-4:]
|
|
33
|
+
self.stdout.write(self.style.SUCCESS(f" API Key: {masked_key}"))
|
|
34
|
+
else:
|
|
35
|
+
self.stdout.write(self.style.ERROR(" API Key: NOT SET"))
|
|
36
|
+
self.stdout.write(
|
|
37
|
+
" Set OG_PILOT_API_KEY env var or OG_PILOT['API_KEY'] in settings"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Check domain
|
|
41
|
+
if config.domain:
|
|
42
|
+
self.stdout.write(self.style.SUCCESS(f" Domain: {config.domain}"))
|
|
43
|
+
else:
|
|
44
|
+
self.stdout.write(self.style.ERROR(" Domain: NOT SET"))
|
|
45
|
+
self.stdout.write(
|
|
46
|
+
" Set OG_PILOT_DOMAIN env var or OG_PILOT['DOMAIN'] in settings"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Show other settings
|
|
50
|
+
self.stdout.write(f" Base URL: {config.base_url}")
|
|
51
|
+
self.stdout.write(f" Open Timeout: {config.open_timeout}s")
|
|
52
|
+
self.stdout.write(f" Read Timeout: {config.read_timeout}s")
|
|
53
|
+
|
|
54
|
+
if options["test"]:
|
|
55
|
+
self.stdout.write("\nTesting API connectivity...")
|
|
56
|
+
try:
|
|
57
|
+
url = og_pilot.create_image(
|
|
58
|
+
template="default",
|
|
59
|
+
title="OG Pilot Test Image",
|
|
60
|
+
)
|
|
61
|
+
self.stdout.write(self.style.SUCCESS(f" Success! Generated URL:"))
|
|
62
|
+
self.stdout.write(f" {url}")
|
|
63
|
+
except ConfigurationError as e:
|
|
64
|
+
self.stdout.write(self.style.ERROR(f" Configuration Error: {e}"))
|
|
65
|
+
except Exception as e:
|
|
66
|
+
self.stdout.write(self.style.ERROR(f" Error: {e}"))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!-- Open Graph meta tags generated by OG Pilot -->
|
|
2
|
+
<meta property="og:title" content="{{ title }}" />
|
|
3
|
+
{% if description %}<meta property="og:description" content="{{ description }}" />{% endif %}
|
|
4
|
+
<meta property="og:image" content="{{ image_url }}" />
|
|
5
|
+
<meta property="og:type" content="website" />
|
|
6
|
+
{% if site_name %}<meta property="og:site_name" content="{{ site_name }}" />{% endif %}
|
|
7
|
+
|
|
8
|
+
<!-- Twitter Card meta tags -->
|
|
9
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
10
|
+
<meta name="twitter:title" content="{{ title }}" />
|
|
11
|
+
{% if description %}<meta name="twitter:description" content="{{ description }}" />{% endif %}
|
|
12
|
+
<meta name="twitter:image" content="{{ image_url }}" />
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Django template tags for OG Pilot
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OG Pilot Template Tags
|
|
3
|
+
|
|
4
|
+
Django template tags for generating OG Pilot images.
|
|
5
|
+
|
|
6
|
+
Usage in templates:
|
|
7
|
+
{% load og_pilot_tags %}
|
|
8
|
+
|
|
9
|
+
<!-- Generate image URL -->
|
|
10
|
+
{% og_pilot_image template="blog_post" title="My Post" as og_url %}
|
|
11
|
+
<meta property="og:image" content="{{ og_url }}" />
|
|
12
|
+
|
|
13
|
+
<!-- Or use the simple tag directly -->
|
|
14
|
+
<meta property="og:image" content="{% og_pilot_url template='default' title='Hello' %}" />
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
from django import template
|
|
20
|
+
|
|
21
|
+
import og_pilot
|
|
22
|
+
|
|
23
|
+
register = template.Library()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@register.simple_tag
|
|
27
|
+
def og_pilot_url(
|
|
28
|
+
title: str,
|
|
29
|
+
template: str = "default",
|
|
30
|
+
iat: int | None = None,
|
|
31
|
+
**kwargs,
|
|
32
|
+
) -> str:
|
|
33
|
+
"""
|
|
34
|
+
Generate an OG Pilot image URL.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
title: The title for the OG image (required)
|
|
38
|
+
template: Template name to use (default: "default")
|
|
39
|
+
iat: Issue time for cache busting. If not provided, uses current day.
|
|
40
|
+
**kwargs: Additional template parameters
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The generated image URL
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
{% og_pilot_url title="My Page Title" template="blog_post" author="John" %}
|
|
47
|
+
"""
|
|
48
|
+
params = {
|
|
49
|
+
"template": template,
|
|
50
|
+
"title": title,
|
|
51
|
+
**kwargs,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Use current day for cache busting if not provided
|
|
55
|
+
if iat is None:
|
|
56
|
+
# Round to start of day for daily cache busting
|
|
57
|
+
iat = int(time.time()) // 86400 * 86400
|
|
58
|
+
|
|
59
|
+
return og_pilot.create_image(params, iat=iat)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@register.simple_tag
|
|
63
|
+
def og_pilot_image(
|
|
64
|
+
title: str,
|
|
65
|
+
template: str = "default",
|
|
66
|
+
iat: int | None = None,
|
|
67
|
+
**kwargs,
|
|
68
|
+
) -> str:
|
|
69
|
+
"""
|
|
70
|
+
Alias for og_pilot_url for semantic clarity.
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
{% og_pilot_image title="My Blog Post" template="blog" as og_url %}
|
|
74
|
+
"""
|
|
75
|
+
return og_pilot_url(title=title, template=template, iat=iat, **kwargs)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@register.inclusion_tag("og_pilot/meta_tags.html")
|
|
79
|
+
def og_pilot_meta_tags(
|
|
80
|
+
title: str,
|
|
81
|
+
description: str = "",
|
|
82
|
+
template: str = "default",
|
|
83
|
+
site_name: str = "",
|
|
84
|
+
**kwargs,
|
|
85
|
+
) -> dict:
|
|
86
|
+
"""
|
|
87
|
+
Render complete Open Graph meta tags with OG Pilot image.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
title: Page title
|
|
91
|
+
description: Page description
|
|
92
|
+
template: OG Pilot template name
|
|
93
|
+
site_name: Site name for og:site_name
|
|
94
|
+
**kwargs: Additional template parameters
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Context dict for the template
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
{% og_pilot_meta_tags title="My Page" description="Description" %}
|
|
101
|
+
"""
|
|
102
|
+
image_url = og_pilot_url(title=title, template=template, **kwargs)
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
"title": title,
|
|
106
|
+
"description": description,
|
|
107
|
+
"image_url": image_url,
|
|
108
|
+
"site_name": site_name,
|
|
109
|
+
}
|
og_pilot/exceptions.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OG Pilot Exceptions
|
|
3
|
+
|
|
4
|
+
Custom exceptions for the OG Pilot SDK.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OgPilotError(Exception):
|
|
9
|
+
"""Base exception for all OG Pilot errors."""
|
|
10
|
+
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConfigurationError(OgPilotError):
|
|
15
|
+
"""Raised when there's a configuration problem."""
|
|
16
|
+
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RequestError(OgPilotError):
|
|
21
|
+
"""Raised when an API request fails."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, message: str, status_code: int | None = None):
|
|
24
|
+
super().__init__(message)
|
|
25
|
+
self.status_code = status_code
|
og_pilot/jwt_encoder.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OG Pilot JWT Encoder
|
|
3
|
+
|
|
4
|
+
JWT encoding utilities using HS256 algorithm.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import jwt
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
ALGORITHM = "HS256"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def encode(payload: dict, secret: str) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Encode a payload as a JWT token using HS256 algorithm.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
payload: Dictionary containing the JWT claims
|
|
19
|
+
secret: Secret key for signing
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Encoded JWT token string
|
|
23
|
+
"""
|
|
24
|
+
return jwt.encode(payload, secret, algorithm=ALGORITHM)
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: og-pilot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the OG Pilot Open Graph image generator with Django integration
|
|
5
|
+
Project-URL: Homepage, https://ogpilot.com
|
|
6
|
+
Project-URL: Documentation, https://ogpilot.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/sunergos-ro/og-pilot-python
|
|
8
|
+
Project-URL: Issues, https://github.com/sunergos-ro/og-pilot-python/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/sunergos-ro/og-pilot-python/commits/main
|
|
10
|
+
Author-email: Sunergos IT LLC <office@sunergos.ro>, Raul Popadineti <raul@sunergos.ro>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: django,meta-tags,og-image,og-pilot,open-graph,seo,social-media
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Environment :: Web Environment
|
|
16
|
+
Classifier: Framework :: Django
|
|
17
|
+
Classifier: Framework :: Django :: 4.2
|
|
18
|
+
Classifier: Framework :: Django :: 5.0
|
|
19
|
+
Classifier: Framework :: Django :: 5.1
|
|
20
|
+
Classifier: Intended Audience :: Developers
|
|
21
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
22
|
+
Classifier: Operating System :: OS Independent
|
|
23
|
+
Classifier: Programming Language :: Python :: 3
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
28
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
29
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
30
|
+
Requires-Python: >=3.10
|
|
31
|
+
Requires-Dist: pyjwt>=2.0.0
|
|
32
|
+
Requires-Dist: requests>=2.25.0
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: django>=4.2; extra == 'dev'
|
|
35
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: responses>=0.23.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: types-requests>=2.31.0; extra == 'dev'
|
|
41
|
+
Provides-Extra: django
|
|
42
|
+
Requires-Dist: django>=4.2; extra == 'django'
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# OG Pilot Python
|
|
46
|
+
|
|
47
|
+
A Python client for generating OG Pilot Open Graph images via signed JWTs, with first-class Django integration.
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install og-pilot
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For Django integration:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install og-pilot[django]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Quick Start
|
|
62
|
+
|
|
63
|
+
### Basic Usage
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import og_pilot
|
|
67
|
+
|
|
68
|
+
# Configure globally (reads from OG_PILOT_API_KEY and OG_PILOT_DOMAIN env vars by default)
|
|
69
|
+
og_pilot.configure(
|
|
70
|
+
api_key="your-api-key",
|
|
71
|
+
domain="example.com"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Generate an image URL
|
|
75
|
+
image_url = og_pilot.create_image(
|
|
76
|
+
template="blog_post",
|
|
77
|
+
title="How to Build Amazing OG Images",
|
|
78
|
+
description="A complete guide to social media previews",
|
|
79
|
+
author_name="Jane Smith",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
print(image_url)
|
|
83
|
+
# https://ogpilot.com/api/v1/images?token=eyJ...
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Using Environment Variables
|
|
87
|
+
|
|
88
|
+
The SDK automatically reads from environment variables:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
export OG_PILOT_API_KEY="your-api-key"
|
|
92
|
+
export OG_PILOT_DOMAIN="example.com"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import og_pilot
|
|
97
|
+
|
|
98
|
+
# No configuration needed - uses env vars
|
|
99
|
+
url = og_pilot.create_image(title="My Page", template="default")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Cache Busting with `iat`
|
|
103
|
+
|
|
104
|
+
By default, OG Pilot caches images indefinitely. Use `iat` (issued at) to refresh the cache:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
import time
|
|
108
|
+
from datetime import datetime
|
|
109
|
+
|
|
110
|
+
# Using Unix timestamp
|
|
111
|
+
url = og_pilot.create_image(
|
|
112
|
+
title="My Post",
|
|
113
|
+
template="blog",
|
|
114
|
+
iat=int(time.time()) # Changes daily
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Using datetime
|
|
118
|
+
url = og_pilot.create_image(
|
|
119
|
+
title="My Post",
|
|
120
|
+
template="blog",
|
|
121
|
+
iat=datetime.now()
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Get JSON Metadata
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
data = og_pilot.create_image(
|
|
129
|
+
title="Hello OG Pilot",
|
|
130
|
+
template="page",
|
|
131
|
+
json=True
|
|
132
|
+
)
|
|
133
|
+
print(data) # {"url": "...", "width": 1200, "height": 630, ...}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Custom Client Instance
|
|
137
|
+
|
|
138
|
+
For multiple configurations or dependency injection:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from og_pilot import create_client
|
|
142
|
+
|
|
143
|
+
client = create_client(
|
|
144
|
+
api_key="your-api-key",
|
|
145
|
+
domain="example.com",
|
|
146
|
+
open_timeout=10,
|
|
147
|
+
read_timeout=30,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
url = client.create_image({"template": "default", "title": "Hello"})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Django Integration
|
|
154
|
+
|
|
155
|
+
### 1. Add to Installed Apps
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
# settings.py
|
|
159
|
+
INSTALLED_APPS = [
|
|
160
|
+
# ...
|
|
161
|
+
'og_pilot.django',
|
|
162
|
+
]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 2. Configure Settings
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# settings.py
|
|
169
|
+
|
|
170
|
+
# Option 1: Using settings dict
|
|
171
|
+
OG_PILOT = {
|
|
172
|
+
'API_KEY': 'your-api-key', # or use OG_PILOT_API_KEY env var
|
|
173
|
+
'DOMAIN': 'example.com', # or use OG_PILOT_DOMAIN env var
|
|
174
|
+
# Optional:
|
|
175
|
+
# 'BASE_URL': 'https://ogpilot.com',
|
|
176
|
+
# 'OPEN_TIMEOUT': 5,
|
|
177
|
+
# 'READ_TIMEOUT': 10,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# Option 2: Using environment variables (no settings needed)
|
|
181
|
+
# Just set OG_PILOT_API_KEY and OG_PILOT_DOMAIN
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### 3. Verify Configuration
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
python manage.py og_pilot_check
|
|
188
|
+
python manage.py og_pilot_check --test # Also sends a test request
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 4. Use in Templates
|
|
192
|
+
|
|
193
|
+
```html
|
|
194
|
+
{% load og_pilot_tags %}
|
|
195
|
+
|
|
196
|
+
<!DOCTYPE html>
|
|
197
|
+
<html>
|
|
198
|
+
<head>
|
|
199
|
+
<!-- Option 1: Generate URL and use manually -->
|
|
200
|
+
{% og_pilot_image title=page.title template="blog_post" as og_image_url %}
|
|
201
|
+
<meta property="og:image" content="{{ og_image_url }}" />
|
|
202
|
+
<meta property="og:title" content="{{ page.title }}" />
|
|
203
|
+
|
|
204
|
+
<!-- Option 2: Simple tag (outputs URL directly) -->
|
|
205
|
+
<meta property="og:image" content="{% og_pilot_url title=page.title template='default' %}" />
|
|
206
|
+
|
|
207
|
+
<!-- Option 3: Complete meta tags (requires template) -->
|
|
208
|
+
{% og_pilot_meta_tags title=page.title description=page.description template="blog" %}
|
|
209
|
+
</head>
|
|
210
|
+
<body>
|
|
211
|
+
...
|
|
212
|
+
</body>
|
|
213
|
+
</html>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### 5. Use in Views
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from django.shortcuts import render
|
|
220
|
+
import og_pilot
|
|
221
|
+
|
|
222
|
+
def blog_post(request, slug):
|
|
223
|
+
post = get_object_or_404(Post, slug=slug)
|
|
224
|
+
|
|
225
|
+
og_image_url = og_pilot.create_image(
|
|
226
|
+
template="blog_post",
|
|
227
|
+
title=post.title,
|
|
228
|
+
description=post.excerpt,
|
|
229
|
+
author_name=post.author.name,
|
|
230
|
+
publish_date=post.published_at.strftime("%Y-%m-%d"),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return render(request, 'blog/post.html', {
|
|
234
|
+
'post': post,
|
|
235
|
+
'og_image_url': og_image_url,
|
|
236
|
+
})
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Custom Meta Tags Template
|
|
240
|
+
|
|
241
|
+
Create `templates/og_pilot/meta_tags.html` in your project to customize the output of `{% og_pilot_meta_tags %}`:
|
|
242
|
+
|
|
243
|
+
```html
|
|
244
|
+
<!-- templates/og_pilot/meta_tags.html -->
|
|
245
|
+
<meta property="og:title" content="{{ title }}" />
|
|
246
|
+
<meta property="og:description" content="{{ description }}" />
|
|
247
|
+
<meta property="og:image" content="{{ image_url }}" />
|
|
248
|
+
<meta property="og:type" content="article" />
|
|
249
|
+
<meta property="og:site_name" content="{{ site_name }}" />
|
|
250
|
+
|
|
251
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
252
|
+
<meta name="twitter:title" content="{{ title }}" />
|
|
253
|
+
<meta name="twitter:description" content="{{ description }}" />
|
|
254
|
+
<meta name="twitter:image" content="{{ image_url }}" />
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Configuration Options
|
|
258
|
+
|
|
259
|
+
| Option | Environment Variable | Default | Description |
|
|
260
|
+
|--------|---------------------|---------|-------------|
|
|
261
|
+
| `api_key` | `OG_PILOT_API_KEY` | None | Your OG Pilot API key (required) |
|
|
262
|
+
| `domain` | `OG_PILOT_DOMAIN` | None | Your registered domain (required) |
|
|
263
|
+
| `base_url` | - | `https://ogpilot.com` | OG Pilot API URL |
|
|
264
|
+
| `open_timeout` | - | `5` | Connection timeout (seconds) |
|
|
265
|
+
| `read_timeout` | - | `10` | Read timeout (seconds) |
|
|
266
|
+
|
|
267
|
+
## Error Handling
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
from og_pilot import create_image
|
|
271
|
+
from og_pilot.exceptions import ConfigurationError, RequestError
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
url = create_image(title="My Post", template="blog")
|
|
275
|
+
except ConfigurationError as e:
|
|
276
|
+
# Missing API key or domain
|
|
277
|
+
print(f"Configuration error: {e}")
|
|
278
|
+
except RequestError as e:
|
|
279
|
+
# API request failed
|
|
280
|
+
print(f"Request error: {e}")
|
|
281
|
+
if e.status_code:
|
|
282
|
+
print(f"Status code: {e.status_code}")
|
|
283
|
+
except ValueError as e:
|
|
284
|
+
# Missing required parameter (e.g., title)
|
|
285
|
+
print(f"Validation error: {e}")
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## API Reference
|
|
289
|
+
|
|
290
|
+
### Module-level Functions
|
|
291
|
+
|
|
292
|
+
- `og_pilot.configure(**kwargs)` - Configure the global client
|
|
293
|
+
- `og_pilot.reset_config()` - Reset to default configuration
|
|
294
|
+
- `og_pilot.get_config()` - Get the current configuration
|
|
295
|
+
- `og_pilot.client()` - Get a client using global config
|
|
296
|
+
- `og_pilot.create_client(**kwargs)` - Create a new client with custom config
|
|
297
|
+
- `og_pilot.create_image(params, *, json=False, iat=None, headers=None, **kwargs)` - Generate image URL
|
|
298
|
+
|
|
299
|
+
### Client Class
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
from og_pilot import Client, Configuration
|
|
303
|
+
|
|
304
|
+
config = Configuration(api_key="...", domain="...")
|
|
305
|
+
client = Client(config)
|
|
306
|
+
|
|
307
|
+
# Generate URL
|
|
308
|
+
url = client.create_image(
|
|
309
|
+
params={"template": "default", "title": "Hello"},
|
|
310
|
+
json_response=False, # Set True for JSON metadata
|
|
311
|
+
iat=None, # Optional cache busting timestamp
|
|
312
|
+
headers={}, # Optional additional headers
|
|
313
|
+
)
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Development
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# Clone the repository
|
|
320
|
+
git clone https://github.com/sunergos-ro/og-pilot-python.git
|
|
321
|
+
cd og-pilot-python
|
|
322
|
+
|
|
323
|
+
# Create virtual environment
|
|
324
|
+
python -m venv .venv
|
|
325
|
+
source .venv/bin/activate
|
|
326
|
+
|
|
327
|
+
# Install dev dependencies
|
|
328
|
+
pip install -e ".[dev]"
|
|
329
|
+
|
|
330
|
+
# Run tests
|
|
331
|
+
pytest
|
|
332
|
+
|
|
333
|
+
# Run linter
|
|
334
|
+
ruff check .
|
|
335
|
+
|
|
336
|
+
# Run type checker
|
|
337
|
+
mypy og_pilot
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
# Publishing to PyPI
|
|
343
|
+
|
|
344
|
+
This section explains how to publish the package to PyPI so users can install it with `pip install og-pilot`.
|
|
345
|
+
|
|
346
|
+
## Prerequisites
|
|
347
|
+
|
|
348
|
+
1. **Create PyPI Account**: Register at https://pypi.org/account/register/
|
|
349
|
+
|
|
350
|
+
2. **Create API Token**: Go to https://pypi.org/manage/account/token/ and create a token with "Upload packages" scope.
|
|
351
|
+
|
|
352
|
+
3. **Install Build Tools**:
|
|
353
|
+
```bash
|
|
354
|
+
pip install build twine
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Publishing Steps
|
|
358
|
+
|
|
359
|
+
### 1. Update Version
|
|
360
|
+
|
|
361
|
+
Edit `pyproject.toml` and `og_pilot/__init__.py` to update the version number:
|
|
362
|
+
|
|
363
|
+
```python
|
|
364
|
+
# og_pilot/__init__.py
|
|
365
|
+
__version__ = "0.2.0" # New version
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
```toml
|
|
369
|
+
# pyproject.toml
|
|
370
|
+
[project]
|
|
371
|
+
version = "0.2.0"
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### 2. Build the Package
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
# Clean previous builds
|
|
378
|
+
rm -rf dist/ build/ *.egg-info
|
|
379
|
+
|
|
380
|
+
# Build source distribution and wheel
|
|
381
|
+
python -m build
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
This creates:
|
|
385
|
+
- `dist/og_pilot-0.1.0.tar.gz` (source distribution)
|
|
386
|
+
- `dist/og_pilot-0.1.0-py3-none-any.whl` (wheel)
|
|
387
|
+
|
|
388
|
+
### 3. Test on TestPyPI (Optional but Recommended)
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
# Upload to TestPyPI first
|
|
392
|
+
twine upload --repository testpypi dist/*
|
|
393
|
+
|
|
394
|
+
# Test installation from TestPyPI
|
|
395
|
+
pip install --index-url https://test.pypi.org/simple/ og-pilot
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### 4. Upload to PyPI
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
# Upload to production PyPI
|
|
402
|
+
twine upload dist/*
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
You'll be prompted for credentials:
|
|
406
|
+
- Username: `__token__`
|
|
407
|
+
- Password: Your PyPI API token (starts with `pypi-`)
|
|
408
|
+
|
|
409
|
+
### 5. Configure Credentials (Optional)
|
|
410
|
+
|
|
411
|
+
To avoid entering credentials each time, create `~/.pypirc`:
|
|
412
|
+
|
|
413
|
+
```ini
|
|
414
|
+
[distutils]
|
|
415
|
+
index-servers =
|
|
416
|
+
pypi
|
|
417
|
+
testpypi
|
|
418
|
+
|
|
419
|
+
[pypi]
|
|
420
|
+
username = __token__
|
|
421
|
+
password = pypi-YOUR-TOKEN-HERE
|
|
422
|
+
|
|
423
|
+
[testpypi]
|
|
424
|
+
username = __token__
|
|
425
|
+
password = pypi-YOUR-TESTPYPI-TOKEN-HERE
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Then secure it:
|
|
429
|
+
```bash
|
|
430
|
+
chmod 600 ~/.pypirc
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## Automated Publishing with GitHub Actions
|
|
434
|
+
|
|
435
|
+
Create `.github/workflows/publish.yml`:
|
|
436
|
+
|
|
437
|
+
```yaml
|
|
438
|
+
name: Publish to PyPI
|
|
439
|
+
|
|
440
|
+
on:
|
|
441
|
+
release:
|
|
442
|
+
types: [published]
|
|
443
|
+
|
|
444
|
+
jobs:
|
|
445
|
+
publish:
|
|
446
|
+
runs-on: ubuntu-latest
|
|
447
|
+
environment: release
|
|
448
|
+
permissions:
|
|
449
|
+
id-token: write # Required for trusted publishing
|
|
450
|
+
|
|
451
|
+
steps:
|
|
452
|
+
- uses: actions/checkout@v4
|
|
453
|
+
|
|
454
|
+
- name: Set up Python
|
|
455
|
+
uses: actions/setup-python@v5
|
|
456
|
+
with:
|
|
457
|
+
python-version: '3.12'
|
|
458
|
+
|
|
459
|
+
- name: Install build dependencies
|
|
460
|
+
run: pip install build
|
|
461
|
+
|
|
462
|
+
- name: Build package
|
|
463
|
+
run: python -m build
|
|
464
|
+
|
|
465
|
+
- name: Publish to PyPI
|
|
466
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
467
|
+
# Uses trusted publishing - configure at pypi.org
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Setting Up Trusted Publishing
|
|
471
|
+
|
|
472
|
+
1. Go to your PyPI project: https://pypi.org/manage/project/og-pilot/settings/publishing/
|
|
473
|
+
2. Add a new publisher:
|
|
474
|
+
- Owner: `sunergos-ro`
|
|
475
|
+
- Repository: `og-pilot-python`
|
|
476
|
+
- Workflow: `publish.yml`
|
|
477
|
+
- Environment: `release`
|
|
478
|
+
|
|
479
|
+
## Version Numbering
|
|
480
|
+
|
|
481
|
+
Follow [Semantic Versioning](https://semver.org/):
|
|
482
|
+
- `MAJOR.MINOR.PATCH` (e.g., `1.2.3`)
|
|
483
|
+
- MAJOR: Breaking changes
|
|
484
|
+
- MINOR: New features (backward compatible)
|
|
485
|
+
- PATCH: Bug fixes (backward compatible)
|
|
486
|
+
|
|
487
|
+
## Release Checklist
|
|
488
|
+
|
|
489
|
+
- [ ] Update version in `pyproject.toml` and `og_pilot/__init__.py`
|
|
490
|
+
- [ ] Update CHANGELOG (if you have one)
|
|
491
|
+
- [ ] Run tests: `pytest`
|
|
492
|
+
- [ ] Run linter: `ruff check .`
|
|
493
|
+
- [ ] Run type checker: `mypy og_pilot`
|
|
494
|
+
- [ ] Build: `python -m build`
|
|
495
|
+
- [ ] Test locally: `pip install dist/*.whl`
|
|
496
|
+
- [ ] Upload to TestPyPI (optional)
|
|
497
|
+
- [ ] Upload to PyPI
|
|
498
|
+
- [ ] Create GitHub release with tag `v0.1.0`
|
|
499
|
+
|
|
500
|
+
## License
|
|
501
|
+
|
|
502
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
og_pilot/__init__.py,sha256=u-SYq0BdlQbchvCHrD5LCllAFCFj5540mija6b0gXlI,3601
|
|
2
|
+
og_pilot/client.py,sha256=f7IpBwPLgHrmXlRNa2DocV2px1Uf3-DDOOGuiwc5d64,6166
|
|
3
|
+
og_pilot/config.py,sha256=oiGorISJ2TyYg_P9OmFDLg2CCnP-Z2WVZTGSaXymhyw,939
|
|
4
|
+
og_pilot/exceptions.py,sha256=MVoG2d8zHZds2HeTHMxIkFzBSBDfcRGl3sVWGGxqP80,493
|
|
5
|
+
og_pilot/jwt_encoder.py,sha256=ZA9NCedRMKmN8NBm4Z-8iQGhFMw-0C1gKbhHfIUrHP0,445
|
|
6
|
+
og_pilot/django/__init__.py,sha256=GNOD10fPV5Nof1zEo81sOPfisyFEH7_qlUTto0j2Yrs,484
|
|
7
|
+
og_pilot/django/apps.py,sha256=9v1HxlzgXA-rCvyDLbRc1UAfN_ZMRsUDlZkFP-RHLbI,1080
|
|
8
|
+
og_pilot/django/management/__init__.py,sha256=wc5DFEklUo-wB-6VAAmsV5UTbo5s3t936Lu61z4lojs,29
|
|
9
|
+
og_pilot/django/management/commands/__init__.py,sha256=EDBTRj1R3MOkHoQl8JNGEc6RGcr4nXwQfUn-EpE2QVU,31
|
|
10
|
+
og_pilot/django/management/commands/og_pilot_check.py,sha256=rK5gmCO4vIwK60tEkKWP0JjVmv1_1s2OOL6uPaL2mDg,2381
|
|
11
|
+
og_pilot/django/templates/og_pilot/meta_tags.html,sha256=aTgbnMK_nPKeFheoigabGGVkqXlInjawL3tuXW7tggQ,681
|
|
12
|
+
og_pilot/django/templatetags/__init__.py,sha256=VvpRwKhOltVZqavfw8J4GY6Mx6QT0_RY4rSVNlQcmaA,36
|
|
13
|
+
og_pilot/django/templatetags/og_pilot_tags.py,sha256=cVzrSSFh0OIQXyr52tsk3PerReT-ltrSmtBs7IF_RZ0,2656
|
|
14
|
+
og_pilot-0.1.0.dist-info/METADATA,sha256=drIjLG6dKNDvTnHuHjC9QQvFFxiZAW-XlLEBLQO8V1E,12385
|
|
15
|
+
og_pilot-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
16
|
+
og_pilot-0.1.0.dist-info/licenses/LICENSE,sha256=PhtdYW0pdQpFJhyX9F2KyaYgbeu0zhADaEsy2UJUL2k,1072
|
|
17
|
+
og_pilot-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sunergos IT LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|