shotgrid-mcp-server 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ """ShotGrid MCP Server Package."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,7 @@
1
+ """Main entry point for the ShotGrid MCP server."""
2
+
3
+ # Import local modules
4
+ from shotgrid_mcp_server.server import main
5
+
6
+ if __name__ == "__main__":
7
+ main()
@@ -0,0 +1,280 @@
1
+ """ShotGrid connection pool module.
2
+
3
+ This module provides a thread-safe connection pool for ShotGrid API.
4
+ """
5
+
6
+ # Import built-in modules
7
+ import logging
8
+ import os
9
+ import queue
10
+ import threading
11
+ from abc import ABC, abstractmethod
12
+ from typing import Any, Optional, Type
13
+
14
+ # Import third-party modules
15
+ from shotgun_api3 import Shotgun
16
+
17
+ # Import local modules
18
+ from src.shotgrid_mcp_server.mockgun_ext import MockgunExt
19
+
20
+ # Configure logging
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class ShotgunClientFactory(ABC):
25
+ """Abstract factory for creating ShotGrid clients."""
26
+
27
+ @abstractmethod
28
+ def create_client(self) -> Shotgun:
29
+ """Create a new ShotGrid client.
30
+
31
+ Returns:
32
+ Shotgun: A new ShotGrid client instance.
33
+ """
34
+ pass
35
+
36
+
37
+ class RealShotgunFactory(ShotgunClientFactory):
38
+ """Factory for creating real ShotGrid clients."""
39
+
40
+ def __init__(
41
+ self,
42
+ url: str,
43
+ script_name: str,
44
+ script_key: str,
45
+ http_proxy: Optional[str] = None,
46
+ ca_certs: Optional[str] = None,
47
+ ) -> None:
48
+ """Initialize the factory.
49
+
50
+ Args:
51
+ url: ShotGrid server URL
52
+ script_name: Script name for authentication
53
+ script_key: Script key for authentication
54
+ http_proxy: Optional HTTP proxy
55
+ ca_certs: Optional CA certificates path
56
+ """
57
+ self.url = url
58
+ self.script_name = script_name
59
+ self.script_key = script_key
60
+ self.http_proxy = http_proxy
61
+ self.ca_certs = ca_certs
62
+
63
+ def create_client(self) -> Shotgun:
64
+ """Create a real ShotGrid client.
65
+
66
+ Returns:
67
+ Shotgun: A new ShotGrid client instance.
68
+
69
+ Raises:
70
+ Exception: If connection creation fails.
71
+ """
72
+ sg = Shotgun(
73
+ self.url,
74
+ script_name=self.script_name,
75
+ api_key=self.script_key,
76
+ http_proxy=self.http_proxy,
77
+ ca_certs=self.ca_certs,
78
+ )
79
+ sg.connect()
80
+ logger.info("Successfully connected to ShotGrid at %s", self.url)
81
+ return sg
82
+
83
+
84
+ class MockShotgunFactory(ShotgunClientFactory):
85
+ """Factory for creating mock ShotGrid clients."""
86
+
87
+ def __init__(self, schema_path: str, schema_entity_path: str) -> None:
88
+ """Initialize the factory.
89
+
90
+ Args:
91
+ schema_path: Path to schema.json
92
+ schema_entity_path: Path to schema_entity.json
93
+ """
94
+ self.schema_path = schema_path
95
+ self.schema_entity_path = schema_entity_path
96
+
97
+ def create_client(self) -> MockgunExt:
98
+ """Create a mock ShotGrid client.
99
+
100
+ Returns:
101
+ MockgunExt: A new mock ShotGrid client instance.
102
+ """
103
+ # Set schema paths before creating the instance
104
+ MockgunExt.set_schema_paths(self.schema_path, self.schema_entity_path)
105
+ sg = MockgunExt(
106
+ "https://test.shotgunstudio.com",
107
+ script_name="test_script",
108
+ api_key="test_key",
109
+ )
110
+ logger.debug("Created mock ShotGrid connection")
111
+ return sg
112
+
113
+
114
+ class ShotGridConnectionPool:
115
+ """A thread-safe connection pool for ShotGrid API."""
116
+
117
+ _instance: Optional["ShotGridConnectionPool"] = None
118
+ _lock: threading.Lock = threading.Lock()
119
+ _pool_size: int = 10
120
+ _connection_queue: queue.Queue[Shotgun]
121
+ _initialized: bool = False
122
+ _factory: ShotgunClientFactory
123
+
124
+ def __new__(
125
+ cls: Type["ShotGridConnectionPool"], factory: Optional[ShotgunClientFactory] = None
126
+ ) -> "ShotGridConnectionPool":
127
+ """Create a singleton instance of the connection pool.
128
+
129
+ Args:
130
+ factory: Factory for creating ShotGrid clients
131
+
132
+ Returns:
133
+ ShotGridConnectionPool: The singleton instance.
134
+ """
135
+ with cls._lock:
136
+ if cls._instance is None:
137
+ cls._instance = super().__new__(cls)
138
+ cls._instance._pool_size = 10
139
+ cls._instance._connection_queue = queue.Queue(maxsize=10)
140
+ cls._instance._initialized = False
141
+ cls._instance._factory = factory or cls._create_default_factory()
142
+ logger.debug("Created new connection pool instance")
143
+ return cls._instance
144
+
145
+ @staticmethod
146
+ def _create_default_factory() -> ShotgunClientFactory:
147
+ """Create the default ShotGrid client factory.
148
+
149
+ Returns:
150
+ ShotgunClientFactory: The default factory instance.
151
+
152
+ Raises:
153
+ ValueError: If required environment variables are missing.
154
+ """
155
+ url = os.getenv("SHOTGRID_URL")
156
+ script_name = os.getenv("SCRIPT_NAME")
157
+ script_key = os.getenv("SCRIPT_KEY")
158
+
159
+ if not all([url, script_name, script_key]):
160
+ logger.error("Missing required environment variables for ShotGrid connection")
161
+ logger.debug("SHOTGRID_URL: %s", url)
162
+ logger.debug("SCRIPT_NAME: %s", script_name)
163
+ logger.debug("SCRIPT_KEY: %s", script_key)
164
+ raise ValueError("Missing required environment variables for ShotGrid connection")
165
+
166
+ # At this point, we know these values are not None
167
+ assert url is not None
168
+ assert script_name is not None
169
+ assert script_key is not None
170
+
171
+ return RealShotgunFactory(
172
+ url=url,
173
+ script_name=script_name,
174
+ script_key=script_key,
175
+ http_proxy=os.getenv("SHOTGUN_HTTP_PROXY"),
176
+ ca_certs=os.getenv("SHOTGUN_API_CACERTS"),
177
+ )
178
+
179
+ def __init__(self, factory: Optional[ShotgunClientFactory] = None) -> None:
180
+ """Initialize the connection pool.
181
+
182
+ Args:
183
+ factory: Factory for creating ShotGrid clients
184
+ """
185
+ if factory:
186
+ self._factory = factory
187
+ if not self._initialized:
188
+ self._init_pool()
189
+ self._initialized = True
190
+
191
+ def _init_pool(self) -> None:
192
+ """Initialize the connection pool with connections."""
193
+ try:
194
+ for i in range(self._pool_size):
195
+ connection = self._factory.create_client()
196
+ self._connection_queue.put(connection)
197
+ logger.debug("Added connection %d/%d to pool", i + 1, self._pool_size)
198
+ logger.info("Successfully initialized connection pool with %d connections", self._pool_size)
199
+ except Exception as e:
200
+ logger.error("Failed to initialize connection pool: %s", str(e), exc_info=True)
201
+ raise
202
+
203
+ def get_connection(self, timeout: Optional[float] = None) -> Shotgun:
204
+ """Get a connection from the pool.
205
+
206
+ Args:
207
+ timeout: How long to wait for a connection if none are available.
208
+ If None, wait indefinitely.
209
+
210
+ Returns:
211
+ Shotgun: A ShotGrid connection from the pool.
212
+
213
+ Raises:
214
+ queue.Empty: If no connection is available within the timeout period.
215
+ """
216
+ try:
217
+ connection = self._connection_queue.get(timeout=timeout)
218
+ logger.debug("Got connection from pool (available: %d)", self._connection_queue.qsize())
219
+ return connection
220
+ except queue.Empty:
221
+ logger.error("Failed to get connection from pool: timeout after %s seconds", timeout)
222
+ raise
223
+
224
+ def return_connection(self, connection: Shotgun) -> None:
225
+ """Return a connection to the pool.
226
+
227
+ Args:
228
+ connection: Connection to return to the pool.
229
+ """
230
+ try:
231
+ self._connection_queue.put(connection)
232
+ logger.debug("Returned connection to pool (available: %d)", self._connection_queue.qsize())
233
+ except Exception as e:
234
+ logger.error("Failed to return connection to pool: %s", str(e), exc_info=True)
235
+ raise
236
+
237
+
238
+ class ShotGridConnectionContext:
239
+ """Context manager for safely handling ShotGrid connections."""
240
+
241
+ def __init__(
242
+ self,
243
+ pool: Optional[ShotGridConnectionPool] = None,
244
+ factory: Optional[ShotgunClientFactory] = None,
245
+ timeout: Optional[float] = None,
246
+ ) -> None:
247
+ """Initialize the context manager.
248
+
249
+ Args:
250
+ pool: The connection pool to get connections from. If None, creates a new pool.
251
+ factory: Factory for creating ShotGrid clients. If provided, creates a new pool with this factory.
252
+ timeout: How long to wait for a connection if none are available.
253
+ """
254
+ self.pool = pool if pool is not None else ShotGridConnectionPool(factory)
255
+ self.timeout = timeout
256
+ self.connection: Optional[Shotgun] = None
257
+
258
+ def __enter__(self) -> Shotgun:
259
+ """Get a connection from the pool.
260
+
261
+ Returns:
262
+ Shotgun: A ShotGrid connection from the pool.
263
+
264
+ Raises:
265
+ Exception: If connection acquisition fails.
266
+ """
267
+ try:
268
+ self.connection = self.pool.get_connection(timeout=self.timeout)
269
+ return self.connection
270
+ except Exception as e:
271
+ logger.error("Failed to acquire connection: %s", str(e), exc_info=True)
272
+ raise
273
+
274
+ def __exit__(
275
+ self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[Any]
276
+ ) -> None:
277
+ """Return the connection to the pool."""
278
+ if self.connection:
279
+ self.pool.return_connection(self.connection)
280
+ self.connection = None
@@ -0,0 +1,51 @@
1
+ """Constants module for ShotGrid server.
2
+
3
+ This module contains all constant values used throughout the ShotGrid server application.
4
+ """
5
+
6
+ # HTTP Status Codes
7
+ HTTP_200_OK = 200
8
+ HTTP_201_CREATED = 201
9
+ HTTP_400_BAD_REQUEST = 400
10
+ HTTP_404_NOT_FOUND = 404
11
+ HTTP_500_INTERNAL_SERVER_ERROR = 500
12
+
13
+ # Environment Variables
14
+ ENV_SHOTGRID_URL = "SHOTGRID_URL"
15
+ ENV_SCRIPT_KEY = "SCRIPT_KEY"
16
+ ENV_SCRIPT_NAME = "SCRIPT_NAME"
17
+
18
+ # Common entity types
19
+ DEFAULT_ENTITY_TYPES = [
20
+ "Version",
21
+ "Shot",
22
+ "Asset",
23
+ "Task",
24
+ "Sequence",
25
+ "Project",
26
+ "Scene",
27
+ "CustomEntity01",
28
+ "CustomEntity02",
29
+ "CustomEntity03",
30
+ ]
31
+
32
+ # Custom entity types can be added through environment variables
33
+ ENV_CUSTOM_ENTITY_TYPES = "SHOTGRID_CUSTOM_ENTITY_TYPES" # Comma-separated list of custom entity types
34
+ ENTITY_TYPES_ENV_VAR = ENV_CUSTOM_ENTITY_TYPES # Alias for backward compatibility
35
+
36
+ # Batch operation limits
37
+ MAX_BATCH_SIZE = 100 # Maximum number of operations per batch request
38
+ MAX_FUZZY_RANGE = 1000 # Maximum range for fuzzy ID searches
39
+ MAX_ID_RANGE = 10000 # Maximum range for ID-based searches
40
+
41
+ # ShotGrid API Credentials
42
+ ENV_SHOTGRID_URL = "SHOTGRID_URL"
43
+ ENV_SCRIPT_NAME = "SCRIPT_NAME"
44
+ ENV_SCRIPT_KEY = "SCRIPT_KEY"
45
+
46
+ # API Routes
47
+ API_PREFIX = "/api/v1"
48
+ HEALTH_CHECK = "/health"
49
+ ENTITY = "/entity"
50
+ ENTITIES = "/entities"
51
+ DOWNLOAD = "/download"
@@ -0,0 +1,91 @@
1
+ """Logging configuration for the ShotGrid MCP server.
2
+
3
+ This module provides a centralized logging configuration for the entire application.
4
+ """
5
+
6
+ # Import built-in modules
7
+ import logging
8
+ import logging.handlers
9
+ import sys
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ # Import third-party modules
15
+ from platformdirs import PlatformDirs
16
+
17
+ # Create platform dirs instance
18
+ dirs = PlatformDirs("shotgrid-mcp-server")
19
+
20
+
21
+ def get_logger(name: Optional[str] = None) -> logging.Logger:
22
+ """Get a logger instance.
23
+
24
+ Args:
25
+ name: The name of the logger. If None, returns the root logger.
26
+
27
+ Returns:
28
+ logging.Logger: A logger instance.
29
+ """
30
+ logger = logging.getLogger(name)
31
+ if not logger.handlers:
32
+ handler = logging.StreamHandler(sys.stdout)
33
+ formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
34
+ handler.setFormatter(formatter)
35
+ logger.addHandler(handler)
36
+ logger.setLevel(logging.INFO)
37
+ return logger
38
+
39
+
40
+ def setup_logging(log_dir: Optional[str] = None) -> None:
41
+ """Set up logging configuration.
42
+
43
+ Args:
44
+ log_dir: Optional directory to store log files. If not provided,
45
+ logs will be stored in the platform-specific user log directory.
46
+ """
47
+ # Use platform-specific log directory if not specified
48
+ if log_dir is None:
49
+ log_dir = dirs.user_log_dir
50
+
51
+ # Create logs directory if it doesn't exist
52
+ log_path = Path(log_dir)
53
+ log_path.mkdir(parents=True, exist_ok=True)
54
+
55
+ # Generate log filename with timestamp
56
+ timestamp = datetime.now().strftime("%Y%m%d")
57
+ log_file = log_path / f"shotgrid_mcp_server_{timestamp}.log"
58
+
59
+ # Create formatters
60
+ file_formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(pathname)s:%(lineno)d - %(message)s")
61
+ console_formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
62
+
63
+ # Create file handler with rotation
64
+ file_handler = logging.handlers.RotatingFileHandler(
65
+ str(log_file), # Convert Path to str for compatibility
66
+ maxBytes=10 * 1024 * 1024, # 10MB
67
+ backupCount=5,
68
+ encoding="utf-8",
69
+ )
70
+ file_handler.setFormatter(file_formatter)
71
+ file_handler.setLevel(logging.DEBUG)
72
+
73
+ # Create console handler
74
+ console_handler = logging.StreamHandler()
75
+ console_handler.setFormatter(console_formatter)
76
+ console_handler.setLevel(logging.INFO)
77
+
78
+ # Configure root logger
79
+ root_logger = logging.getLogger()
80
+ root_logger.setLevel(logging.DEBUG)
81
+
82
+ # Remove existing handlers to avoid duplicates
83
+ root_logger.handlers.clear()
84
+
85
+ # Add handlers
86
+ root_logger.addHandler(file_handler)
87
+ root_logger.addHandler(console_handler)
88
+
89
+ # Create logger for this application
90
+ logger = logging.getLogger("mcp_shotgrid_server")
91
+ logger.info("Logging system initialized. Log file: %s", log_file)