meta-ads-mcp 0.7.6__py3-none-any.whl → 0.7.8__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.
- meta_ads_mcp/__init__.py +1 -1
- meta_ads_mcp/core/accounts.py +50 -11
- meta_ads_mcp/core/adsets.py +36 -6
- meta_ads_mcp/core/auth.py +38 -2
- meta_ads_mcp/core/pipeboard_auth.py +43 -1
- {meta_ads_mcp-0.7.6.dist-info → meta_ads_mcp-0.7.8.dist-info}/METADATA +1 -1
- {meta_ads_mcp-0.7.6.dist-info → meta_ads_mcp-0.7.8.dist-info}/RECORD +10 -10
- {meta_ads_mcp-0.7.6.dist-info → meta_ads_mcp-0.7.8.dist-info}/WHEEL +0 -0
- {meta_ads_mcp-0.7.6.dist-info → meta_ads_mcp-0.7.8.dist-info}/entry_points.txt +0 -0
- {meta_ads_mcp-0.7.6.dist-info → meta_ads_mcp-0.7.8.dist-info}/licenses/LICENSE +0 -0
meta_ads_mcp/__init__.py
CHANGED
meta_ads_mcp/core/accounts.py
CHANGED
|
@@ -36,27 +36,66 @@ async def get_account_info(access_token: str = None, account_id: str = None) ->
|
|
|
36
36
|
|
|
37
37
|
Args:
|
|
38
38
|
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
39
|
-
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
39
|
+
account_id: Meta Ads account ID (format: act_XXXXXXXXX) - REQUIRED
|
|
40
40
|
"""
|
|
41
|
-
# If no account ID is specified, try to get the first one for the user
|
|
42
41
|
if not account_id:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
return {
|
|
43
|
+
"error": {
|
|
44
|
+
"message": "Account ID is required",
|
|
45
|
+
"details": "Please specify an account_id parameter",
|
|
46
|
+
"example": "Use account_id='act_123456789' or account_id='123456789'"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
50
49
|
|
|
51
50
|
# Ensure account_id has the 'act_' prefix for API compatibility
|
|
52
51
|
if not account_id.startswith("act_"):
|
|
53
52
|
account_id = f"act_{account_id}"
|
|
54
53
|
|
|
54
|
+
# First, check if the account is accessible to the user
|
|
55
|
+
endpoint = "me/adaccounts"
|
|
56
|
+
params = {
|
|
57
|
+
"fields": "id,name,account_id,account_status,amount_spent,balance,currency,age,business_city,business_country_code",
|
|
58
|
+
"limit": 50
|
|
59
|
+
}
|
|
60
|
+
accessible_accounts_data = await make_api_request(endpoint, access_token, params)
|
|
61
|
+
|
|
62
|
+
if "data" in accessible_accounts_data:
|
|
63
|
+
accessible_account_ids = [acc["id"] for acc in accessible_accounts_data["data"]]
|
|
64
|
+
if account_id not in accessible_account_ids:
|
|
65
|
+
# Provide a helpful error message with accessible accounts
|
|
66
|
+
accessible_accounts = [
|
|
67
|
+
{"id": acc["id"], "name": acc["name"]}
|
|
68
|
+
for acc in accessible_accounts_data["data"][:10] # Show first 10
|
|
69
|
+
]
|
|
70
|
+
return {
|
|
71
|
+
"error": {
|
|
72
|
+
"message": f"Account {account_id} is not accessible to your user account",
|
|
73
|
+
"details": "This account either doesn't exist or you don't have permission to access it",
|
|
74
|
+
"accessible_accounts": accessible_accounts,
|
|
75
|
+
"total_accessible_accounts": len(accessible_accounts_data["data"]),
|
|
76
|
+
"suggestion": "Try using one of the accessible account IDs listed above"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
55
80
|
endpoint = f"{account_id}"
|
|
56
81
|
params = {
|
|
57
|
-
"fields": "id,name,account_id,account_status,amount_spent,balance,currency,age,
|
|
82
|
+
"fields": "id,name,account_id,account_status,amount_spent,balance,currency,age,business_city,business_country_code,timezone_name"
|
|
58
83
|
}
|
|
59
84
|
|
|
60
85
|
data = await make_api_request(endpoint, access_token, params)
|
|
61
86
|
|
|
62
|
-
|
|
87
|
+
# Check if the API request returned an error
|
|
88
|
+
if "error" in data:
|
|
89
|
+
return data
|
|
90
|
+
|
|
91
|
+
# Add DSA requirement detection
|
|
92
|
+
if "business_country_code" in data:
|
|
93
|
+
european_countries = ["DE", "FR", "IT", "ES", "NL", "BE", "AT", "IE", "DK", "SE", "FI", "NO"]
|
|
94
|
+
if data["business_country_code"] in european_countries:
|
|
95
|
+
data["dsa_required"] = True
|
|
96
|
+
data["dsa_compliance_note"] = "This account is subject to European DSA (Digital Services Act) requirements"
|
|
97
|
+
else:
|
|
98
|
+
data["dsa_required"] = False
|
|
99
|
+
data["dsa_compliance_note"] = "This account is not subject to European DSA requirements"
|
|
100
|
+
|
|
101
|
+
return data
|
meta_ads_mcp/core/adsets.py
CHANGED
|
@@ -73,7 +73,7 @@ async def get_adset_details(access_token: str = None, adset_id: str = None) -> s
|
|
|
73
73
|
endpoint = f"{adset_id}"
|
|
74
74
|
# Explicitly prioritize frequency_control_specs in the fields request
|
|
75
75
|
params = {
|
|
76
|
-
"fields": "id,name,campaign_id,status,frequency_control_specs{event,interval_days,max_frequency},daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,attribution_spec,destination_type,promoted_object,pacing_type,budget_remaining"
|
|
76
|
+
"fields": "id,name,campaign_id,status,frequency_control_specs{event,interval_days,max_frequency},daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,attribution_spec,destination_type,promoted_object,pacing_type,budget_remaining,dsa_beneficiary"
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
data = await make_api_request(endpoint, access_token, params)
|
|
@@ -103,6 +103,7 @@ async def create_adset(
|
|
|
103
103
|
bid_strategy: str = None,
|
|
104
104
|
start_time: str = None,
|
|
105
105
|
end_time: str = None,
|
|
106
|
+
dsa_beneficiary: str = None,
|
|
106
107
|
access_token: str = None
|
|
107
108
|
) -> str:
|
|
108
109
|
"""
|
|
@@ -123,6 +124,7 @@ async def create_adset(
|
|
|
123
124
|
bid_strategy: Bid strategy (e.g., 'LOWEST_COST', 'LOWEST_COST_WITH_BID_CAP')
|
|
124
125
|
start_time: Start time in ISO 8601 format (e.g., '2023-12-01T12:00:00-0800')
|
|
125
126
|
end_time: End time in ISO 8601 format
|
|
127
|
+
dsa_beneficiary: DSA beneficiary (person/organization benefiting from ads) for European compliance
|
|
126
128
|
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
127
129
|
"""
|
|
128
130
|
# Check required parameters
|
|
@@ -181,16 +183,44 @@ async def create_adset(
|
|
|
181
183
|
if end_time:
|
|
182
184
|
params["end_time"] = end_time
|
|
183
185
|
|
|
186
|
+
# Add DSA beneficiary if provided
|
|
187
|
+
if dsa_beneficiary:
|
|
188
|
+
params["dsa_beneficiary"] = dsa_beneficiary
|
|
189
|
+
|
|
184
190
|
try:
|
|
185
191
|
data = await make_api_request(endpoint, access_token, params, method="POST")
|
|
186
192
|
return json.dumps(data, indent=2)
|
|
187
193
|
except Exception as e:
|
|
188
194
|
error_msg = str(e)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
|
|
196
|
+
# Enhanced error handling for DSA beneficiary issues
|
|
197
|
+
if "permission" in error_msg.lower() or "insufficient" in error_msg.lower():
|
|
198
|
+
return json.dumps({
|
|
199
|
+
"error": "Insufficient permissions to set DSA beneficiary. Please ensure you have business_management permissions.",
|
|
200
|
+
"details": error_msg,
|
|
201
|
+
"params_sent": params,
|
|
202
|
+
"permission_required": True
|
|
203
|
+
}, indent=2)
|
|
204
|
+
elif "dsa_beneficiary" in error_msg.lower() and ("not supported" in error_msg.lower() or "parameter" in error_msg.lower()):
|
|
205
|
+
return json.dumps({
|
|
206
|
+
"error": "DSA beneficiary parameter not supported in this API version. Please set DSA beneficiary manually in Facebook Ads Manager.",
|
|
207
|
+
"details": error_msg,
|
|
208
|
+
"params_sent": params,
|
|
209
|
+
"manual_setup_required": True
|
|
210
|
+
}, indent=2)
|
|
211
|
+
elif "benefits from ads" in error_msg or "DSA beneficiary" in error_msg:
|
|
212
|
+
return json.dumps({
|
|
213
|
+
"error": "DSA beneficiary required for European compliance. Please provide the person or organization that benefits from ads in this ad set.",
|
|
214
|
+
"details": error_msg,
|
|
215
|
+
"params_sent": params,
|
|
216
|
+
"dsa_required": True
|
|
217
|
+
}, indent=2)
|
|
218
|
+
else:
|
|
219
|
+
return json.dumps({
|
|
220
|
+
"error": "Failed to create ad set",
|
|
221
|
+
"details": error_msg,
|
|
222
|
+
"params_sent": params
|
|
223
|
+
}, indent=2)
|
|
194
224
|
|
|
195
225
|
|
|
196
226
|
@mcp_server.tool()
|
meta_ads_mcp/core/auth.py
CHANGED
|
@@ -25,7 +25,7 @@ from .pipeboard_auth import pipeboard_auth_manager
|
|
|
25
25
|
# Auth constants
|
|
26
26
|
# Scope includes pages_show_list and pages_read_engagement to fix issue #16
|
|
27
27
|
# where get_account_pages failed for regular users due to missing page permissions
|
|
28
|
-
AUTH_SCOPE = "
|
|
28
|
+
AUTH_SCOPE = "business_management,public_profile,pages_show_list,pages_read_engagement"
|
|
29
29
|
AUTH_REDIRECT_URI = "http://localhost:8888/callback"
|
|
30
30
|
AUTH_RESPONSE_TYPE = "token"
|
|
31
31
|
|
|
@@ -159,11 +159,41 @@ class AuthManager:
|
|
|
159
159
|
try:
|
|
160
160
|
with open(cache_path, "r") as f:
|
|
161
161
|
data = json.load(f)
|
|
162
|
+
|
|
163
|
+
# Validate the cached data structure
|
|
164
|
+
required_fields = ["access_token", "created_at"]
|
|
165
|
+
if not all(field in data for field in required_fields):
|
|
166
|
+
logger.warning("Cached token data is missing required fields")
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
# Check if the token looks valid (basic format check)
|
|
170
|
+
if not data.get("access_token") or len(data["access_token"]) < 20:
|
|
171
|
+
logger.warning("Cached token appears malformed")
|
|
172
|
+
return False
|
|
173
|
+
|
|
162
174
|
self.token_info = TokenInfo.deserialize(data)
|
|
163
175
|
|
|
164
176
|
# Check if token is expired
|
|
165
177
|
if self.token_info.is_expired():
|
|
166
|
-
logger.info("Cached token is expired")
|
|
178
|
+
logger.info("Cached token is expired, removing cache file")
|
|
179
|
+
# Remove the expired cache file
|
|
180
|
+
try:
|
|
181
|
+
cache_path.unlink()
|
|
182
|
+
logger.info(f"Removed expired token cache: {cache_path}")
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.warning(f"Could not remove expired cache file: {e}")
|
|
185
|
+
self.token_info = None
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
# Additional validation: check if token is too old (more than 60 days)
|
|
189
|
+
current_time = int(time.time())
|
|
190
|
+
if self.token_info.created_at and (current_time - self.token_info.created_at) > (60 * 24 * 3600):
|
|
191
|
+
logger.warning("Cached token is too old (more than 60 days), removing cache file")
|
|
192
|
+
try:
|
|
193
|
+
cache_path.unlink()
|
|
194
|
+
logger.info(f"Removed old token cache: {cache_path}")
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.warning(f"Could not remove old cache file: {e}")
|
|
167
197
|
self.token_info = None
|
|
168
198
|
return False
|
|
169
199
|
|
|
@@ -171,6 +201,12 @@ class AuthManager:
|
|
|
171
201
|
return True
|
|
172
202
|
except Exception as e:
|
|
173
203
|
logger.error(f"Error loading cached token: {e}")
|
|
204
|
+
# If there's any error reading the cache, try to remove the corrupted file
|
|
205
|
+
try:
|
|
206
|
+
cache_path.unlink()
|
|
207
|
+
logger.info(f"Removed corrupted token cache: {cache_path}")
|
|
208
|
+
except Exception as cleanup_error:
|
|
209
|
+
logger.warning(f"Could not remove corrupted cache file: {cleanup_error}")
|
|
174
210
|
return False
|
|
175
211
|
|
|
176
212
|
def _save_token_to_cache(self) -> None:
|
|
@@ -151,6 +151,18 @@ class PipeboardAuthManager:
|
|
|
151
151
|
with open(cache_path, "r") as f:
|
|
152
152
|
logger.debug(f"Reading token cache from {cache_path}")
|
|
153
153
|
data = json.load(f)
|
|
154
|
+
|
|
155
|
+
# Validate the cached data structure
|
|
156
|
+
required_fields = ["access_token"]
|
|
157
|
+
if not all(field in data for field in required_fields):
|
|
158
|
+
logger.warning("Cached token data is missing required fields")
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
# Check if the token looks valid (basic format check)
|
|
162
|
+
if not data.get("access_token") or len(data["access_token"]) < 20:
|
|
163
|
+
logger.warning("Cached token appears malformed")
|
|
164
|
+
return False
|
|
165
|
+
|
|
154
166
|
self.token_info = TokenInfo.deserialize(data)
|
|
155
167
|
|
|
156
168
|
# Log token details (partial token for security)
|
|
@@ -159,7 +171,25 @@ class PipeboardAuthManager:
|
|
|
159
171
|
|
|
160
172
|
# Check if token is expired
|
|
161
173
|
if self.token_info.is_expired():
|
|
162
|
-
logger.info("Cached token is expired")
|
|
174
|
+
logger.info("Cached token is expired, removing cache file")
|
|
175
|
+
# Remove the expired cache file
|
|
176
|
+
try:
|
|
177
|
+
cache_path.unlink()
|
|
178
|
+
logger.info(f"Removed expired token cache: {cache_path}")
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.warning(f"Could not remove expired cache file: {e}")
|
|
181
|
+
self.token_info = None
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
# Additional validation: check if token is too old (more than 60 days)
|
|
185
|
+
current_time = int(time.time())
|
|
186
|
+
if self.token_info.created_at and (current_time - self.token_info.created_at) > (60 * 24 * 3600):
|
|
187
|
+
logger.warning("Cached token is too old (more than 60 days), removing cache file")
|
|
188
|
+
try:
|
|
189
|
+
cache_path.unlink()
|
|
190
|
+
logger.info(f"Removed old token cache: {cache_path}")
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.warning(f"Could not remove old cache file: {e}")
|
|
163
193
|
self.token_info = None
|
|
164
194
|
return False
|
|
165
195
|
|
|
@@ -174,9 +204,21 @@ class PipeboardAuthManager:
|
|
|
174
204
|
logger.debug(f"Raw cache file content (first 100 chars): {raw_content[:100]}")
|
|
175
205
|
except Exception as e2:
|
|
176
206
|
logger.error(f"Could not read raw cache file: {e2}")
|
|
207
|
+
# If there's any error reading the cache, try to remove the corrupted file
|
|
208
|
+
try:
|
|
209
|
+
cache_path.unlink()
|
|
210
|
+
logger.info(f"Removed corrupted token cache: {cache_path}")
|
|
211
|
+
except Exception as cleanup_error:
|
|
212
|
+
logger.warning(f"Could not remove corrupted cache file: {cleanup_error}")
|
|
177
213
|
return False
|
|
178
214
|
except Exception as e:
|
|
179
215
|
logger.error(f"Error loading cached token: {e}")
|
|
216
|
+
# If there's any error reading the cache, try to remove the corrupted file
|
|
217
|
+
try:
|
|
218
|
+
cache_path.unlink()
|
|
219
|
+
logger.info(f"Removed corrupted token cache: {cache_path}")
|
|
220
|
+
except Exception as cleanup_error:
|
|
221
|
+
logger.warning(f"Could not remove corrupted cache file: {cleanup_error}")
|
|
180
222
|
return False
|
|
181
223
|
|
|
182
224
|
def _save_token_to_cache(self) -> None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meta-ads-mcp
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.8
|
|
4
4
|
Summary: Model Context Protocol (MCP) plugin for interacting with Meta Ads API
|
|
5
5
|
Project-URL: Homepage, https://github.com/pipeboard-co/meta-ads-mcp
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/pipeboard-co/meta-ads-mcp/issues
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
meta_ads_mcp/__init__.py,sha256=
|
|
1
|
+
meta_ads_mcp/__init__.py,sha256=BiSbN8PrAeLKejVnkAfsz06KLVM59vQJmehwssDnEzc,1492
|
|
2
2
|
meta_ads_mcp/__main__.py,sha256=XaQt3iXftG_7f0Zu7Wop9SeFgrD2WBn0EQOaPMc27d8,207
|
|
3
3
|
meta_ads_mcp/core/__init__.py,sha256=6nYdue6yRepkt6JTAoPGhGbS51qfDSvmczRrDwYOG6A,1709
|
|
4
|
-
meta_ads_mcp/core/accounts.py,sha256=
|
|
4
|
+
meta_ads_mcp/core/accounts.py,sha256=4IAdGLZ4WE4j4pGW6E0qaXcXqbUIW6Wk2kuQUtlmRTQ,4030
|
|
5
5
|
meta_ads_mcp/core/ads.py,sha256=aaK70mgfhBJRXr4cdkKag5mjYzvHuHpRttJvTMzPk4Y,36156
|
|
6
6
|
meta_ads_mcp/core/ads_library.py,sha256=BBGVbtjO5eFV42iiY3XPU-wIV8HupzUKpHgPBrydSvU,3232
|
|
7
|
-
meta_ads_mcp/core/adsets.py,sha256=
|
|
7
|
+
meta_ads_mcp/core/adsets.py,sha256=vY5JNHmGK1a_sQ5B1LnjxLYXzs5_jOajTTjWHRDJ4_Y,12518
|
|
8
8
|
meta_ads_mcp/core/api.py,sha256=aAzM6Q75VQOFXtr5D-mDmBRhxWK4wsiODsJYnR3mpDI,14994
|
|
9
|
-
meta_ads_mcp/core/auth.py,sha256=
|
|
9
|
+
meta_ads_mcp/core/auth.py,sha256=2CjFbxpJM3OR3OzCipB8l_-l2xQ1nioGfdI3ZDMnjHM,23629
|
|
10
10
|
meta_ads_mcp/core/authentication.py,sha256=-AJxa3a5ZshRCvmJThBaNwCAJ1D2_qOgUkvu539c_MY,10159
|
|
11
11
|
meta_ads_mcp/core/budget_schedules.py,sha256=UxseExsvKAiPwfDCY9aycT4kys4xqeNytyq-yyDOxrs,2901
|
|
12
12
|
meta_ads_mcp/core/callback_server.py,sha256=LIAJv9DW--83kdZ7VWWZal8xEprYjRZ8iug4rMczYbQ,9372
|
|
@@ -15,14 +15,14 @@ meta_ads_mcp/core/duplication.py,sha256=UUmTDFx9o5ZsPQG2Rb9c4ZyuKUVN3FfTjebfTIHH
|
|
|
15
15
|
meta_ads_mcp/core/http_auth_integration.py,sha256=lGpKhfzJcyWugBcYEvypY-qnlt-3UDBLqh7xAUH0DGw,12473
|
|
16
16
|
meta_ads_mcp/core/insights.py,sha256=Qr1wq-1VT9HwF4w11rIRM4IBYdrksJ-6EOv3p33ZtKw,2613
|
|
17
17
|
meta_ads_mcp/core/openai_deep_research.py,sha256=Ocs8bmNNBLZQLmWfL6azlC3RNzevVzV5WgcEp4H2wdY,13240
|
|
18
|
-
meta_ads_mcp/core/pipeboard_auth.py,sha256=
|
|
18
|
+
meta_ads_mcp/core/pipeboard_auth.py,sha256=yT2-e9dpmkMOg6rMJWaQAMg4DZu4RxlDV5zkTm-G--o,25168
|
|
19
19
|
meta_ads_mcp/core/reports.py,sha256=Dv3hfsPOR7IZ9WrYrKd_6SNgZl-USIphg7knva3UYAw,5747
|
|
20
20
|
meta_ads_mcp/core/resources.py,sha256=-zIIfZulpo76vcKv6jhAlQq91cR2SZ3cjYZt3ek3x0w,1236
|
|
21
21
|
meta_ads_mcp/core/server.py,sha256=WhbAag7xdhbGcp7rnU4sKhqXJ8Slapa_ba3T23Yp_2U,17889
|
|
22
22
|
meta_ads_mcp/core/targeting.py,sha256=3HW1qirEdwaQurlBZGenbIwawcb5J06ghJKRfgu9ZEs,6318
|
|
23
23
|
meta_ads_mcp/core/utils.py,sha256=ofKUhyo-5SZoJVuBeTVFPPQCffk0UKpwmDMrd8qQxNc,8715
|
|
24
|
-
meta_ads_mcp-0.7.
|
|
25
|
-
meta_ads_mcp-0.7.
|
|
26
|
-
meta_ads_mcp-0.7.
|
|
27
|
-
meta_ads_mcp-0.7.
|
|
28
|
-
meta_ads_mcp-0.7.
|
|
24
|
+
meta_ads_mcp-0.7.8.dist-info/METADATA,sha256=HjCu4iRgkgfEv5WYS3-E6CchfrkassZHZjMVbgfOphE,20409
|
|
25
|
+
meta_ads_mcp-0.7.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
26
|
+
meta_ads_mcp-0.7.8.dist-info/entry_points.txt,sha256=Dv2RkoBjRJBqj6CyhwqGIiwPCD-SCL1-7B9-zmVRuv0,57
|
|
27
|
+
meta_ads_mcp-0.7.8.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
28
|
+
meta_ads_mcp-0.7.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|