cloudflare-analytics 0.1.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,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cloudflare Analytics GraphQL API client.
|
|
3
|
+
|
|
4
|
+
API docs: https://developers.cloudflare.com/analytics/graphql-api/
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from .client import CloudflareAnalyticsClient, GraphQLResponse, get_analytics_client
|
|
11
|
+
|
|
12
|
+
logging.basicConfig(
|
|
13
|
+
level=os.environ.get("LOG_LEVEL", "INFO").upper(),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"CloudflareAnalyticsClient",
|
|
20
|
+
"GraphQLResponse",
|
|
21
|
+
"get_analytics_client",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main():
|
|
26
|
+
logger.info("Cloudflare Analytics Client - use as a library")
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cloudflare Analytics GraphQL API client.
|
|
3
|
+
|
|
4
|
+
API docs: https://developers.cloudflare.com/analytics/graphql-api/
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
from tenacity import (
|
|
13
|
+
before_sleep_log,
|
|
14
|
+
retry,
|
|
15
|
+
retry_if_exception_type,
|
|
16
|
+
stop_after_attempt,
|
|
17
|
+
wait_exponential,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
_analytics_client: "CloudflareAnalyticsClient | None" = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GraphQLResponse(BaseModel):
|
|
26
|
+
data: dict[str, Any] | None = None
|
|
27
|
+
errors: list[dict[str, Any]] | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CloudflareAnalyticsClient:
|
|
31
|
+
"""
|
|
32
|
+
Client for interacting with the Cloudflare GraphQL Analytics API using httpx.
|
|
33
|
+
|
|
34
|
+
Example usage:
|
|
35
|
+
|
|
36
|
+
from cloudflare_analytics import CloudflareAnalyticsClient
|
|
37
|
+
|
|
38
|
+
client = CloudflareAnalyticsClient(api_token="your_token")
|
|
39
|
+
|
|
40
|
+
query = '''
|
|
41
|
+
query GetStreamMinutes($accountTag: string!, $start: Date, $end: Date) {
|
|
42
|
+
viewer {
|
|
43
|
+
accounts(filter: { accountTag: $accountTag }) {
|
|
44
|
+
streamMinutesViewedAdaptiveGroups(
|
|
45
|
+
filter: { date_geq: $start, date_lt: $end }
|
|
46
|
+
orderBy: [date_ASC]
|
|
47
|
+
) {
|
|
48
|
+
dimensions { date }
|
|
49
|
+
sum { minutesViewed }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
'''
|
|
55
|
+
|
|
56
|
+
response = client.query(
|
|
57
|
+
query,
|
|
58
|
+
variables={
|
|
59
|
+
"accountTag": "your_account_id",
|
|
60
|
+
"start": "2025-10-01",
|
|
61
|
+
"end": "2025-10-28"
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if response.errors:
|
|
66
|
+
logger.error("graphql errors", extra={"errors": response.errors})
|
|
67
|
+
elif response.data:
|
|
68
|
+
groups = response.data["viewer"]["accounts"][0]["streamMinutesViewedAdaptiveGroups"]
|
|
69
|
+
for group in groups:
|
|
70
|
+
minutes = group["sum"]["minutesViewed"]
|
|
71
|
+
date = group["dimensions"]["date"]
|
|
72
|
+
logger.info("stream data", extra={"date": date, "minutes": minutes})
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, api_token: str):
|
|
76
|
+
if not api_token:
|
|
77
|
+
raise ValueError("API token must be provided.")
|
|
78
|
+
|
|
79
|
+
self.api_token = api_token
|
|
80
|
+
self.base_url = "https://api.cloudflare.com/client/v4"
|
|
81
|
+
|
|
82
|
+
@retry(
|
|
83
|
+
stop=stop_after_attempt(6),
|
|
84
|
+
wait=wait_exponential(multiplier=1, min=0, max=32),
|
|
85
|
+
retry=retry_if_exception_type(httpx.HTTPError),
|
|
86
|
+
before_sleep=before_sleep_log(logger, logging.INFO),
|
|
87
|
+
reraise=True,
|
|
88
|
+
)
|
|
89
|
+
def _make_request(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
90
|
+
"""
|
|
91
|
+
Internal method to make the HTTP POST request to the GraphQL endpoint.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
payload: Dictionary containing query and optional variables
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The JSON response as a dictionary
|
|
98
|
+
"""
|
|
99
|
+
url = f"{self.base_url}/graphql"
|
|
100
|
+
headers = {"Authorization": f"Bearer {self.api_token}"}
|
|
101
|
+
|
|
102
|
+
logger.debug(
|
|
103
|
+
"cloudflare graphql request", extra={"url": url, "payload": payload}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
response = httpx.post(url, json=payload, headers=headers)
|
|
107
|
+
response.raise_for_status()
|
|
108
|
+
|
|
109
|
+
json_data = response.json()
|
|
110
|
+
logger.info(
|
|
111
|
+
"cloudflare graphql response", extra={"status": response.status_code}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return json_data
|
|
115
|
+
|
|
116
|
+
def query(
|
|
117
|
+
self, query: str, variables: dict[str, Any] | None = None
|
|
118
|
+
) -> GraphQLResponse:
|
|
119
|
+
"""Execute a GraphQL query against the Cloudflare Analytics API.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
query: GraphQL query string
|
|
123
|
+
variables: Optional variables for the query
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
GraphQLResponse containing data or errors
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
httpx.HTTPError: If the request fails after retries
|
|
130
|
+
"""
|
|
131
|
+
payload: dict[str, Any] = {"query": query}
|
|
132
|
+
if variables:
|
|
133
|
+
payload["variables"] = variables
|
|
134
|
+
|
|
135
|
+
json_data = self._make_request(payload)
|
|
136
|
+
|
|
137
|
+
return GraphQLResponse(
|
|
138
|
+
data=json_data.get("data"), errors=json_data.get("errors")
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_analytics_client(api_token: str) -> CloudflareAnalyticsClient:
|
|
143
|
+
"""
|
|
144
|
+
Get or create the global Cloudflare Analytics client instance.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
api_token: Cloudflare API token
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
CloudflareAnalyticsClient: The configured analytics client instance
|
|
151
|
+
"""
|
|
152
|
+
global _analytics_client
|
|
153
|
+
|
|
154
|
+
if _analytics_client is None:
|
|
155
|
+
_analytics_client = CloudflareAnalyticsClient(api_token=api_token)
|
|
156
|
+
|
|
157
|
+
return _analytics_client
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: cloudflare-analytics
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Cloudflare Analytics GraphQL API client
|
|
5
|
+
Keywords: cloudflare,analytics,graphql
|
|
6
|
+
Author: Michael Bianco
|
|
7
|
+
Author-email: Michael Bianco <mike@mikebian.co>
|
|
8
|
+
Requires-Dist: httpx>=0.27.0
|
|
9
|
+
Requires-Dist: pydantic>=2.0.0
|
|
10
|
+
Requires-Dist: tenacity>=8.0.0
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Project-URL: Repository, https://github.com/iloveitaly/cloudflare-analytics
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Python Cloudflare Analytics Client
|
|
16
|
+
|
|
17
|
+
A Python client for interacting with the [Cloudflare GraphQL Analytics API](https://developers.cloudflare.com/analytics/graphql-api/).
|
|
18
|
+
|
|
19
|
+
> **Note:** The official Cloudflare Python library does not support the GraphQL Analytics API endpoints. This library provides dedicated support for querying Cloudflare analytics data.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv add cloudflare-analytics
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from cloudflare_analytics import client
|
|
31
|
+
|
|
32
|
+
# Execute a GraphQL query
|
|
33
|
+
query = '''
|
|
34
|
+
query GetStreamMinutes($accountTag: string!, $start: Date, $end: Date) {
|
|
35
|
+
viewer {
|
|
36
|
+
accounts(filter: { accountTag: $accountTag }) {
|
|
37
|
+
streamMinutesViewedAdaptiveGroups(
|
|
38
|
+
filter: { date_geq: $start, date_lt: $end }
|
|
39
|
+
orderBy: [date_ASC]
|
|
40
|
+
) {
|
|
41
|
+
dimensions { date }
|
|
42
|
+
sum { minutesViewed }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
'''
|
|
48
|
+
|
|
49
|
+
response = client.query(
|
|
50
|
+
query,
|
|
51
|
+
variables={
|
|
52
|
+
"accountTag": "your_account_id",
|
|
53
|
+
"start": "2025-10-01",
|
|
54
|
+
"end": "2025-10-28"
|
|
55
|
+
},
|
|
56
|
+
api_token="your_cloudflare_api_token"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if response.errors:
|
|
60
|
+
print(f"Errors: {response.errors}")
|
|
61
|
+
elif response.data:
|
|
62
|
+
groups = response.data["viewer"]["accounts"][0]["streamMinutesViewedAdaptiveGroups"]
|
|
63
|
+
for group in groups:
|
|
64
|
+
minutes = group["sum"]["minutesViewed"]
|
|
65
|
+
date = group["dimensions"]["date"]
|
|
66
|
+
print(f"Date: {date}, Minutes: {minutes}")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Features
|
|
70
|
+
|
|
71
|
+
- Simple, clean API for Cloudflare GraphQL Analytics
|
|
72
|
+
- Built-in retry logic with exponential backoff
|
|
73
|
+
- Type-safe responses using Pydantic models
|
|
74
|
+
- Comprehensive error handling
|
|
75
|
+
|
|
76
|
+
## [MIT License](LICENSE.md)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
cloudflare_analytics/__init__.py,sha256=YjAPfeP55HEhOmpQfevStNG1svRvgJ6k7ScTr7kOoDQ,525
|
|
2
|
+
cloudflare_analytics/client.py,sha256=GGcl7fZtCJMKldi91AMxe1jOlh6G6I_ni4KC-2VASys,4517
|
|
3
|
+
cloudflare_analytics-0.1.1.dist-info/WHEEL,sha256=Pi5uDq5Fdo_Rr-HD5h9BiPn9Et29Y9Sh8NhcJNnFU1c,79
|
|
4
|
+
cloudflare_analytics-0.1.1.dist-info/entry_points.txt,sha256=GHeI44VGvKgmiH2T36QidTL4bZDbkcCqjPUi0FYOEdk,68
|
|
5
|
+
cloudflare_analytics-0.1.1.dist-info/METADATA,sha256=HlgNWdQDxYIvDy8K2svZgEtXamvq7OAB8GYdXZ13l0o,2086
|
|
6
|
+
cloudflare_analytics-0.1.1.dist-info/RECORD,,
|