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 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"
@@ -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
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.