airbyte-source-google-search-console 1.6.0.dev202503282247__py3-none-any.whl → 1.6.0rc1__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.
Files changed (17) hide show
  1. {airbyte_source_google_search_console-1.6.0.dev202503282247.dist-info → airbyte_source_google_search_console-1.6.0rc1.dist-info}/METADATA +4 -4
  2. {airbyte_source_google_search_console-1.6.0.dev202503282247.dist-info → airbyte_source_google_search_console-1.6.0rc1.dist-info}/RECORD +11 -15
  3. {airbyte_source_google_search_console-1.6.0.dev202503282247.dist-info → airbyte_source_google_search_console-1.6.0rc1.dist-info}/WHEEL +1 -1
  4. source_google_search_console/components.py +81 -0
  5. source_google_search_console/config_migrations.py +4 -1
  6. source_google_search_console/manifest.yaml +341 -0
  7. source_google_search_console/run.py +47 -9
  8. source_google_search_console/service_account_authenticator.py +3 -1
  9. source_google_search_console/source.py +26 -53
  10. source_google_search_console/streams.py +3 -58
  11. source_google_search_console/schemas/search_analytics_by_country.json +0 -41
  12. source_google_search_console/schemas/search_analytics_keyword_page_report_minimal_dimensions.json +0 -54
  13. source_google_search_console/schemas/search_analytics_keyword_site_report_by_page_minimal_dimensions.json +0 -50
  14. source_google_search_console/schemas/search_analytics_keyword_site_report_by_site_minimal_dimensions.json +0 -50
  15. source_google_search_console/schemas/sitemaps.json +0 -61
  16. source_google_search_console/schemas/sites.json +0 -14
  17. {airbyte_source_google_search_console-1.6.0.dev202503282247.dist-info → airbyte_source_google_search_console-1.6.0rc1.dist-info}/entry_points.txt +0 -0
@@ -1,19 +1,19 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: airbyte-source-google-search-console
3
- Version: 1.6.0.dev202503282247
3
+ Version: 1.6.0rc1
4
4
  Summary: Source implementation for Google Search Console.
5
5
  License: Elv2
6
6
  Author: Airbyte
7
7
  Author-email: contact@airbyte.io
8
- Requires-Python: >=3.9,<3.12
8
+ Requires-Python: >=3.10,<3.12
9
9
  Classifier: License :: Other/Proprietary License
10
10
  Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.9
12
11
  Classifier: Programming Language :: Python :: 3.10
13
12
  Classifier: Programming Language :: Python :: 3.11
14
- Requires-Dist: airbyte-cdk (>=1,<2)
13
+ Requires-Dist: airbyte-cdk (>=6,<7)
15
14
  Requires-Dist: google-api-python-client (==2.105.0)
16
15
  Requires-Dist: google-auth (==2.23.3)
16
+ Requires-Dist: pendulum (>=3.1.0,<4.0.0)
17
17
  Project-URL: Documentation, https://docs.airbyte.com/integrations/sources/google-search-console
18
18
  Project-URL: Homepage, https://airbyte.com
19
19
  Project-URL: Repository, https://github.com/airbytehq/airbyte
@@ -1,29 +1,25 @@
1
1
  source_google_search_console/__init__.py,sha256=HQCPu-CK7XmVDtP9rmTdB2XyraVCc6pv9pw38-O8y48,1191
2
- source_google_search_console/config_migrations.py,sha256=9C8QoELMAxt8NHiEY4fZMCYy5PwoOSUd6vSwoC1B3FM,4135
2
+ source_google_search_console/components.py,sha256=5o8kH2xwYUvk3yjnSd6okJVF2KBUqUIF4V97xRpPAyI,2803
3
+ source_google_search_console/config_migrations.py,sha256=Cl4SUdJpAf6wMM_vVhqjjU89NfUq9LIGJ9zNrWiBk-A,4235
3
4
  source_google_search_console/exceptions.py,sha256=iD3jYC4WxVCEKGsqQ7Vaj1tbjhJZ4S5mnSDnwFJdsIQ,1097
4
- source_google_search_console/run.py,sha256=q6O2iXoxtrdgtodCaanqTO2eKzUvXF2iDCfmCxaPE24,462
5
+ source_google_search_console/manifest.yaml,sha256=zoPWyJ7gwOOK6yKAk4H92cD-N7Xg3iHtp5hTT_e80QA,10974
6
+ source_google_search_console/run.py,sha256=TBkPlseTERarkj6wL8AMEKgm5Xsb2drnltPVH6257-M,2195
5
7
  source_google_search_console/schemas/search_analytics_all_fields.json,sha256=iQxRh_c_yz3uGofqpo1KX571TMmzYjKScb0PtI6SN_Q,1729
6
- source_google_search_console/schemas/search_analytics_by_country.json,sha256=xvUVjGRy63dsc7c0O-Kg7DUzybRpD-r_-VYhPmDBw_o,1491
7
8
  source_google_search_console/schemas/search_analytics_by_date.json,sha256=meCbWDayc1y0q-Lu-CAdjQVnsM8xZBX3BdF129UC1P8,1388
8
9
  source_google_search_console/schemas/search_analytics_by_device.json,sha256=VtoFjmmv9rx-uhSFaRn0wm4LeSxRIaexrxg2Spvbneo,1525
9
10
  source_google_search_console/schemas/search_analytics_by_page.json,sha256=KyUojZc4Lv3hPswxIJzUL5QDNsbvSugGjl_uHGF7Am4,1473
10
11
  source_google_search_console/schemas/search_analytics_by_query.json,sha256=mYc1Fu1A5TWLZCApZSjjfs_urW5BRUCPQI3rw3yQjt4,1439
11
12
  source_google_search_console/schemas/search_analytics_keyword_page_report.json,sha256=hCQZbpW9UNiCOrnUMkj4oQdzY0k8Hqoh1XkuKHvyvZw,1681
12
- source_google_search_console/schemas/search_analytics_keyword_page_report_minimal_dimensions.json,sha256=hCQZbpW9UNiCOrnUMkj4oQdzY0k8Hqoh1XkuKHvyvZw,1681
13
13
  source_google_search_console/schemas/search_analytics_keyword_site_report_by_page.json,sha256=lFpbShmTQhFyySc_JCdIOqMP9RfOIA15fpQN9xNXtTI,1626
14
- source_google_search_console/schemas/search_analytics_keyword_site_report_by_page_minimal_dimensions.json,sha256=lFpbShmTQhFyySc_JCdIOqMP9RfOIA15fpQN9xNXtTI,1626
15
14
  source_google_search_console/schemas/search_analytics_keyword_site_report_by_site.json,sha256=AVUl5x9jLL0FcStbTR-FMy-_A7uM-VlrJ-kdCBXWz_g,1755
16
- source_google_search_console/schemas/search_analytics_keyword_site_report_by_site_minimal_dimensions.json,sha256=AVUl5x9jLL0FcStbTR-FMy-_A7uM-VlrJ-kdCBXWz_g,1755
17
15
  source_google_search_console/schemas/search_analytics_page_report.json,sha256=-b0Y0LenTchS0q9A2aQ4hIjUjXkYF8erOtyrTMhf6MM,1776
18
16
  source_google_search_console/schemas/search_analytics_site_report_by_page.json,sha256=hWKHkm1reqGGu1dNcWBe6_XkZ5tK-UaiymrYRVgxRxI,1515
19
17
  source_google_search_console/schemas/search_analytics_site_report_by_site.json,sha256=rAh6LuNy7nCrrNM9MTd0qxAVc886ecQaqWRgV63OfyA,1408
20
- source_google_search_console/schemas/sitemaps.json,sha256=coyPSZCAfzMheybfRp4WPAZCp5JF2KGRF2rWK8oC080,1775
21
- source_google_search_console/schemas/sites.json,sha256=WNiCRuStPL1YkJiFa8FEbNJmqaERAOf9Yow6ygIumvo,383
22
- source_google_search_console/service_account_authenticator.py,sha256=gjUxt0xFxj82uviCQNTsA1Jlee__UDhYNjE7bRO1G0U,1227
23
- source_google_search_console/source.py,sha256=4UJW4KUVGo5WxH9ewWBVSemXH17Iqp-eaNRtUfLNET8,10431
18
+ source_google_search_console/service_account_authenticator.py,sha256=pAWKAXfwfTY3xkXvQJH0EyFphFULdCIcC47YXYTO9X8,1307
19
+ source_google_search_console/source.py,sha256=7FD4ciRrsptU7ZIxAU2xLC37bgjKWzNkflE9ybmgpXM,9113
24
20
  source_google_search_console/spec.json,sha256=WYtFvaSqWYGm1Dt2yV9G92U78Q94rh9oarbxJe3H7xo,8470
25
- source_google_search_console/streams.py,sha256=1rgtlcVUyAP3oKRNQ34qIh6rwTzvlr636GiEy0RPQLs,20508
26
- airbyte_source_google_search_console-1.6.0.dev202503282247.dist-info/METADATA,sha256=YyTR_gsHe3OcxaiSStl2wtO8Wa5Vsfm16QJ6miZG8JA,5645
27
- airbyte_source_google_search_console-1.6.0.dev202503282247.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
28
- airbyte_source_google_search_console-1.6.0.dev202503282247.dist-info/entry_points.txt,sha256=DMcgc9bCX-Vt6hm_68pa77qS3eGdeMhg-UdlFc-XKUM,85
29
- airbyte_source_google_search_console-1.6.0.dev202503282247.dist-info/RECORD,,
21
+ source_google_search_console/streams.py,sha256=T0eqhmxGPDAfFNMXFfG_vM4aYHFaHAHHAoj6s5XnjnI,18760
22
+ airbyte_source_google_search_console-1.6.0rc1.dist-info/METADATA,sha256=smSxRuSxL5aXxK-OcjVGdpR_jLJN_-dMEtsKyMu5Oww,5624
23
+ airbyte_source_google_search_console-1.6.0rc1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
24
+ airbyte_source_google_search_console-1.6.0rc1.dist-info/entry_points.txt,sha256=DMcgc9bCX-Vt6hm_68pa77qS3eGdeMhg-UdlFc-XKUM,85
25
+ airbyte_source_google_search_console-1.6.0rc1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.1
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -0,0 +1,81 @@
1
+ #
2
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Mapping, Optional
7
+
8
+ from airbyte_cdk.sources.declarative.migrations.state_migration import StateMigration
9
+
10
+
11
+ @dataclass
12
+ class NestedSubstreamStateMigration(StateMigration):
13
+ """
14
+ We require a custom state migration because SearchAnalytics streams contain two nested levels of
15
+ substreams. The existing LegacyToPerPartitionStateMigration only handles one level.
16
+
17
+ Legacy state format is as follows:
18
+ {
19
+ "date": "2025-05-28",
20
+ "https://www.example.com/": {
21
+ "web": {
22
+ "date": "2025-05-25"
23
+ },
24
+ "news": {
25
+ "date": "2023-05-22"
26
+ }
27
+ }
28
+ }
29
+
30
+ The resulting migrated per-partition state is:
31
+ {
32
+ "use_global_cursor": false,
33
+ "states": [
34
+ {
35
+ "partition": {
36
+ "search_type": "web",
37
+ "site_url": "https://www.example.com/"
38
+ },
39
+ "cursor": {
40
+ "date": "2025-05-25"
41
+ }
42
+ },
43
+ {
44
+ "partition": {
45
+ "search_type": "news",
46
+ "site_url": "https://www.example.com/"
47
+ },
48
+ "cursor": {
49
+ "date": "2023-05-22"
50
+ }
51
+ }],
52
+ "state": {
53
+ "date": "2025-05-25"
54
+ }
55
+ }
56
+ """
57
+
58
+ def should_migrate(self, stream_state: Mapping[str, Any]) -> bool:
59
+ return len(stream_state) > 0 and "states" not in stream_state
60
+
61
+ def migrate(self, stream_state: Mapping[str, Any]) -> Mapping[str, Any]:
62
+ global_state: Optional[Mapping[str, Any]] = None
63
+ per_partition_state = []
64
+ for site_url_key, search_type_state in stream_state.items():
65
+ if site_url_key == "date":
66
+ # The legacy state also contains a global cursor value under the `date` key which equates
67
+ # to global state.
68
+ #
69
+ # However, the Python implementation does not appear to be implemented
70
+ # correctly and simply saves the state of the last seen partition. Since I don't trust the
71
+ # legacy value and in the current implementation global state is applied to partitions
72
+ # without an existing value, I'm making a conscious choice to not migrate the global value.
73
+ continue
74
+ else:
75
+ site_url = site_url_key
76
+ for search_type_key, cursor in search_type_state.items():
77
+ per_partition_state.append({"partition": {"site_url": site_url, "search_type": search_type_key}, "cursor": cursor})
78
+ return {
79
+ "use_global_cursor": False,
80
+ "states": per_partition_state,
81
+ }
@@ -6,8 +6,11 @@
6
6
  import logging
7
7
  from typing import Any, List, Mapping
8
8
 
9
+ import orjson
10
+
9
11
  from airbyte_cdk.config_observation import create_connector_config_control_message
10
12
  from airbyte_cdk.entrypoint import AirbyteEntrypoint
13
+ from airbyte_cdk.models import AirbyteMessageSerializer
11
14
  from airbyte_cdk.sources import Source
12
15
  from airbyte_cdk.sources.message import InMemoryMessageRepository, MessageRepository
13
16
 
@@ -77,7 +80,7 @@ class MigrateCustomReports:
77
80
  cls.message_repository.emit_message(create_connector_config_control_message(migrated_config))
78
81
  # emit the Airbyte Control Message from message queue to stdout
79
82
  for message in cls.message_repository._message_queue:
80
- print(message.json(exclude_unset=True))
83
+ print(orjson.dumps(AirbyteMessageSerializer.dump(message)).decode())
81
84
 
82
85
  @classmethod
83
86
  def migrate(cls, args: List[str], source: Source) -> None:
@@ -0,0 +1,341 @@
1
+ version: 6.44.0
2
+
3
+ type: DeclarativeSource
4
+
5
+ check:
6
+ type: CheckStream
7
+ stream_names:
8
+ - sites
9
+
10
+ definitions:
11
+ oauth_authenticator:
12
+ type: OAuthAuthenticator
13
+ client_id: "{{ config.get('authorization', {}).get('client_id') }}"
14
+ client_secret: "{{ config.get('authorization', {}).get('client_secret') }}"
15
+ refresh_token: "{{ config.get('authorization', {}).get('refresh_token') }}"
16
+ token_refresh_endpoint: "https://oauth2.googleapis.com/token"
17
+
18
+ jwt_profile_assertion_oauth_authenticator:
19
+ type: OAuthAuthenticator
20
+ token_refresh_endpoint: https://oauth2.googleapis.com/token
21
+ refresh_request_headers:
22
+ Content-Type: application/x-www-form-urlencoded
23
+ use_profile_assertion: true
24
+ profile_assertion:
25
+ type: JwtAuthenticator
26
+ secret_key: "{{ json_loads(config.get('authorization', {}).get('service_account_info', {})).get('private_key') }}"
27
+ algorithm: "RS256"
28
+ token_duration: 3600
29
+ jwt_payload:
30
+ aud: "{{ json_loads(config.get('authorization', {}).get('service_account_info', {})).get('token_uri') }}"
31
+ iss: "{{ json_loads(config.get('authorization', {}).get('service_account_info', {})).get('client_email') }}"
32
+ additional_jwt_payload:
33
+ scope: "https://www.googleapis.com/auth/webmasters.readonly"
34
+
35
+ selective_authenticator:
36
+ type: SelectiveAuthenticator
37
+ authenticator_selection_path: ["authorization", "auth_type"]
38
+ authenticators:
39
+ Client: "#/definitions/oauth_authenticator"
40
+ Service: "#/definitions/jwt_profile_assertion_oauth_authenticator"
41
+
42
+ search_analytics_by_country_stream:
43
+ type: DeclarativeStream
44
+ name: search_analytics_by_country
45
+ primary_key:
46
+ - site_url
47
+ - date
48
+ - country
49
+ - search_type
50
+ retriever:
51
+ type: SimpleRetriever
52
+ requester:
53
+ type: HttpRequester
54
+ url_base: https://www.googleapis.com/webmasters/v3
55
+ path: "/sites/{{ sanitize_url(stream_partition.get('site_url')) }}/searchAnalytics/query"
56
+ http_method: POST
57
+ authenticator: "#/definitions/selective_authenticator"
58
+ request_headers:
59
+ Content-Type: "application/json"
60
+ request_body_json:
61
+ startDate: "{{ stream_interval.get('start_time') }}"
62
+ endDate: "{{ stream_interval.get('end_time') }}"
63
+ dimensions: ["date", "country"]
64
+ type: "{{ stream_partition.get('search_type') }}"
65
+ aggregationType: auto
66
+ dataState: "{{ config.get('data_state', 'final') }}"
67
+ # Currently relying on the default error handler behavior. Two pieces of functionality not covered are
68
+ # - Silently skipping over 403 permissions errors and relying on partial success reporting
69
+ # - Retrying 400 errors with aggregation_type=auto instead of failing outright
70
+ paginator:
71
+ type: DefaultPaginator
72
+ page_token_option:
73
+ type: RequestOption
74
+ field_name: startRow
75
+ inject_into: body_json
76
+ page_size_option:
77
+ type: RequestOption
78
+ field_name: rowLimit
79
+ inject_into: body_json
80
+ pagination_strategy:
81
+ type: OffsetIncrement
82
+ page_size: 25000
83
+ inject_on_first_request: true
84
+ record_selector:
85
+ type: RecordSelector
86
+ extractor:
87
+ type: DpathExtractor
88
+ field_path:
89
+ - rows
90
+ partition_router:
91
+ - type: ListPartitionRouter
92
+ values: "{{ config['site_urls'] }}"
93
+ cursor_field: site_url
94
+ - type: ListPartitionRouter
95
+ values:
96
+ - web
97
+ - news
98
+ - image
99
+ - video
100
+ - discover
101
+ - googleNews
102
+ cursor_field: search_type
103
+ incremental_sync:
104
+ type: DatetimeBasedCursor
105
+ cursor_field: date
106
+ cursor_datetime_formats:
107
+ - "%Y-%m-%d"
108
+ datetime_format: "%Y-%m-%d"
109
+ start_datetime:
110
+ type: MinMaxDatetime
111
+ datetime: "{{ config.get('start_date', '2021-01-01') }}"
112
+ datetime_format: "%Y-%m-%d"
113
+ end_datetime:
114
+ type: MinMaxDatetime
115
+ datetime: "{{ config.get('end_date', today_utc()) }}"
116
+ datetime_format: "%Y-%m-%d"
117
+ step: P3D
118
+ cursor_granularity: P1D
119
+ transformations:
120
+ - type: AddFields
121
+ fields:
122
+ - path:
123
+ - site_url
124
+ value: "{{ stream_partition['site_url'] }}"
125
+ - path:
126
+ - search_type
127
+ value: "{{ stream_partition['search_type'] }}"
128
+ # The values in the 'keys' array in the record correspond to the same order that the dimensions
129
+ # are requested in the API request. For example, if the request body was `dimensions: ["date", "country"]`,
130
+ # then the first value of `keys` is placed under the `date` field. These arrays are always be the same length
131
+ # After extracting the keys, the `keys` array is removed from the record.
132
+ - type: AddFields
133
+ fields:
134
+ - path:
135
+ - date
136
+ value: "{{ record['keys'][0] }}"
137
+ - path:
138
+ - country
139
+ value: "{{ record['keys'][1] }}"
140
+ - type: RemoveFields
141
+ field_pointers:
142
+ - - keys
143
+ schema_loader:
144
+ type: InlineSchemaLoader
145
+ schema:
146
+ $ref: "#/schemas/search_analytics_by_country"
147
+ state_migrations:
148
+ - type: CustomStateMigration
149
+ class_name: source_google_search_console.components.NestedSubstreamStateMigration
150
+
151
+ sites_stream:
152
+ type: DeclarativeStream
153
+ name: sites
154
+ retriever:
155
+ type: SimpleRetriever
156
+ requester:
157
+ type: HttpRequester
158
+ url_base: https://www.googleapis.com/webmasters/v3
159
+ path: "/sites/{{ sanitize_url(stream_partition.get('site_url')) }}"
160
+ http_method: GET
161
+ authenticator: "#/definitions/selective_authenticator"
162
+ record_selector:
163
+ type: RecordSelector
164
+ extractor:
165
+ type: DpathExtractor
166
+ field_path: []
167
+ partition_router:
168
+ - type: ListPartitionRouter
169
+ values: "{{ config['site_urls'] }}"
170
+ cursor_field: site_url
171
+ schema_loader:
172
+ type: InlineSchemaLoader
173
+ schema:
174
+ $ref: "#/schemas/sites"
175
+
176
+ sitemaps_stream:
177
+ type: DeclarativeStream
178
+ name: sitemaps
179
+ retriever:
180
+ type: SimpleRetriever
181
+ requester:
182
+ type: HttpRequester
183
+ url_base: https://www.googleapis.com/webmasters/v3
184
+ path: "/sites/{{ sanitize_url(stream_partition.get('site_url')) }}/sitemaps"
185
+ http_method: GET
186
+ authenticator: "#/definitions/selective_authenticator"
187
+ record_selector:
188
+ type: RecordSelector
189
+ extractor:
190
+ type: DpathExtractor
191
+ field_path:
192
+ - "sitemap"
193
+ partition_router:
194
+ - type: ListPartitionRouter
195
+ values: "{{ config['site_urls'] }}"
196
+ cursor_field: site_url
197
+ schema_loader:
198
+ type: InlineSchemaLoader
199
+ schema:
200
+ $ref: "#/schemas/sitemaps"
201
+
202
+ streams:
203
+ - "#/definitions/search_analytics_by_country_stream"
204
+ - "#/definitions/sites_stream"
205
+ - "#/definitions/sitemaps_stream"
206
+
207
+ schemas:
208
+ search_analytics_by_country:
209
+ $schema: "http://json-schema.org/draft-07/schema#"
210
+ type: object
211
+ properties:
212
+ site_url:
213
+ description: The URL of the site for which the search analytics data is being reported.
214
+ type:
215
+ - "null"
216
+ - string
217
+ search_type:
218
+ description: >-
219
+ The type of search (web search, image search, video search, etc.) for
220
+ which the data is being reported.
221
+ type:
222
+ - "null"
223
+ - string
224
+ date:
225
+ description: The date for which the search analytics data is being reported.
226
+ type:
227
+ - "null"
228
+ - string
229
+ format: date
230
+ country:
231
+ description: The country for which the search analytics data is being reported.
232
+ type:
233
+ - "null"
234
+ - string
235
+ clicks:
236
+ description: >-
237
+ The number of times users clicked on the search result for a specific
238
+ country.
239
+ type:
240
+ - "null"
241
+ - integer
242
+ impressions:
243
+ description: >-
244
+ The total number of times a search result was shown in search results for
245
+ a specific country.
246
+ type:
247
+ - "null"
248
+ - integer
249
+ ctr:
250
+ description: >-
251
+ The click-through rate, i.e., the ratio of clicks to impressions for a
252
+ specific country.
253
+ type:
254
+ - "null"
255
+ - number
256
+ multipleOf: 1.e-25
257
+ position:
258
+ description: >-
259
+ The average position at which the site's search result appeared for a
260
+ specific country.
261
+ type:
262
+ - "null"
263
+ - number
264
+ multipleOf: 1.e-25
265
+ sites:
266
+ $schema: "http://json-schema.org/draft-07/schema#"
267
+ type: object
268
+ properties:
269
+ siteUrl:
270
+ description: "The URL of the site data being fetched"
271
+ type: ["null", "string"]
272
+ permissionLevel:
273
+ description: "The user's permission level for the site (owner, full, restricted, etc.)"
274
+ type: ["null", "string"]
275
+ sitemaps:
276
+ $schema: "http://json-schema.org/draft-07/schema#"
277
+ type: object
278
+ properties:
279
+ path:
280
+ description: "Path to the sitemap file"
281
+ type:
282
+ - "null"
283
+ - string
284
+ lastSubmitted:
285
+ description: "Timestamp when the sitemap was last submitted"
286
+ type:
287
+ - "null"
288
+ - string
289
+ format: date-time
290
+ isPending:
291
+ description: "Flag indicating if the sitemap is pending for processing"
292
+ type:
293
+ - "null"
294
+ - boolean
295
+ isSitemapsIndex:
296
+ description: "Flag indicating if the data represents a sitemap index"
297
+ type:
298
+ - "null"
299
+ - boolean
300
+ type:
301
+ description: "Type of the sitemap"
302
+ type:
303
+ - "null"
304
+ - string
305
+ lastDownloaded:
306
+ description: "Timestamp when the sitemap was last downloaded"
307
+ type:
308
+ - "null"
309
+ - string
310
+ format: date-time
311
+ warnings:
312
+ description: "Warnings encountered while processing the sitemaps"
313
+ type:
314
+ - "null"
315
+ - string
316
+ errors:
317
+ description: "Errors encountered while processing the sitemaps"
318
+ type:
319
+ - "null"
320
+ - string
321
+ contents:
322
+ description: "Data related to the sitemap contents"
323
+ type: array
324
+ items:
325
+ type: object
326
+ properties:
327
+ type:
328
+ description: "Type of the sitemap content"
329
+ type:
330
+ - "null"
331
+ - string
332
+ submitted:
333
+ description: "Number of submitted sitemap URLs"
334
+ type:
335
+ - "null"
336
+ - string
337
+ indexed:
338
+ description: "Number of indexed sitemap URLs"
339
+ type:
340
+ - "null"
341
+ - string
@@ -1,18 +1,56 @@
1
1
  #
2
- # Copyright (c) 2023 Airbyte, Inc., all rights reserved.
2
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
3
3
  #
4
4
 
5
-
6
5
  import sys
6
+ import traceback
7
+ from datetime import datetime
8
+ from typing import List
9
+
10
+ from orjson import orjson
7
11
 
8
- from airbyte_cdk.entrypoint import launch
12
+ from airbyte_cdk.entrypoint import AirbyteEntrypoint, launch, logger
13
+ from airbyte_cdk.exception_handler import init_uncaught_exception_handler
14
+ from airbyte_cdk.models import AirbyteErrorTraceMessage, AirbyteMessage, AirbyteMessageSerializer, AirbyteTraceMessage, TraceType, Type
9
15
  from source_google_search_console import SourceGoogleSearchConsole
10
16
  from source_google_search_console.config_migrations import MigrateCustomReports
11
17
 
12
18
 
13
- def run():
14
- source = SourceGoogleSearchConsole()
15
- # migrate config at runtime
16
- MigrateCustomReports.migrate(sys.argv[1:], source)
17
- # run the connector
18
- launch(source, sys.argv[1:])
19
+ def _get_source(args: List[str]):
20
+ catalog_path = AirbyteEntrypoint.extract_catalog(args)
21
+ config_path = AirbyteEntrypoint.extract_config(args)
22
+ state_path = AirbyteEntrypoint.extract_state(args)
23
+ try:
24
+ return SourceGoogleSearchConsole(
25
+ SourceGoogleSearchConsole.read_catalog(catalog_path) if catalog_path else None,
26
+ SourceGoogleSearchConsole.read_config(config_path) if config_path else None,
27
+ SourceGoogleSearchConsole.read_state(state_path) if state_path else None,
28
+ )
29
+ except Exception as error:
30
+ print(
31
+ orjson.dumps(
32
+ AirbyteMessageSerializer.dump(
33
+ AirbyteMessage(
34
+ type=Type.TRACE,
35
+ trace=AirbyteTraceMessage(
36
+ type=TraceType.ERROR,
37
+ emitted_at=int(datetime.now().timestamp() * 1000),
38
+ error=AirbyteErrorTraceMessage(
39
+ message=f"Error starting the sync. This could be due to an invalid configuration or catalog. Please contact Support for assistance. Error: {error}",
40
+ stack_trace=traceback.format_exc(),
41
+ ),
42
+ ),
43
+ )
44
+ )
45
+ ).decode()
46
+ )
47
+ return None
48
+
49
+
50
+ def run() -> None:
51
+ init_uncaught_exception_handler(logger)
52
+ _args = sys.argv[1:]
53
+ source = _get_source(_args)
54
+ if source:
55
+ MigrateCustomReports.migrate(sys.argv[1:], source)
56
+ launch(source, _args)
@@ -1,6 +1,7 @@
1
1
  #
2
2
  # Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3
3
  #
4
+ import json
4
5
 
5
6
  import requests
6
7
  from google.auth.transport.requests import Request
@@ -21,7 +22,8 @@ class ServiceAccountAuthenticator(AuthBase):
21
22
 
22
23
  def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
23
24
  try:
24
- credentials: Credentials = Credentials.from_service_account_info(self.service_account_info, scopes=self.scopes).with_subject(
25
+ service_account_info = json.loads(self.service_account_info)
26
+ credentials: Credentials = Credentials.from_service_account_info(service_account_info, scopes=self.scopes).with_subject(
25
27
  self.email
26
28
  )
27
29
  if not credentials.valid:
@@ -11,8 +11,9 @@ import jsonschema
11
11
  import pendulum
12
12
  import requests
13
13
 
14
- from airbyte_cdk.models import FailureType, SyncMode
15
- from airbyte_cdk.sources import AbstractSource
14
+ from airbyte_cdk.models import ConfiguredAirbyteCatalog, FailureType, SyncMode
15
+ from airbyte_cdk.sources.declarative.yaml_declarative_source import YamlDeclarativeSource
16
+ from airbyte_cdk.sources.source import TState
16
17
  from airbyte_cdk.sources.streams import Stream
17
18
  from airbyte_cdk.sources.streams.http.requests_native_auth import Oauth2Authenticator
18
19
  from airbyte_cdk.utils import AirbyteTracedException
@@ -25,23 +26,17 @@ from source_google_search_console.exceptions import (
25
26
  from source_google_search_console.service_account_authenticator import ServiceAccountAuthenticator
26
27
  from source_google_search_console.streams import (
27
28
  SearchAnalyticsAllFields,
28
- SearchAnalyticsByCountry,
29
29
  SearchAnalyticsByCustomDimensions,
30
30
  SearchAnalyticsByDate,
31
31
  SearchAnalyticsByDevice,
32
32
  SearchAnalyticsByPage,
33
33
  SearchAnalyticsByQuery,
34
34
  SearchAnalyticsKeywordPageReport,
35
- SearchAnalyticsKeywordPageReportMinimalDimensions,
36
35
  SearchAnalyticsKeywordSiteReportByPage,
37
- SearchAnalyticsKeywordSiteReportByPageMinimalDimensions,
38
36
  SearchAnalyticsKeywordSiteReportBySite,
39
- SearchAnalyticsKeywordSiteReportBySiteMinimalDimensions,
40
37
  SearchAnalyticsPageReport,
41
38
  SearchAnalyticsSiteReportByPage,
42
39
  SearchAnalyticsSiteReportBySite,
43
- Sitemaps,
44
- Sites,
45
40
  )
46
41
 
47
42
 
@@ -58,7 +53,10 @@ custom_reports_schema = {
58
53
  }
59
54
 
60
55
 
61
- class SourceGoogleSearchConsole(AbstractSource):
56
+ class SourceGoogleSearchConsole(YamlDeclarativeSource):
57
+ def __init__(self, catalog: Optional[ConfiguredAirbyteCatalog], config: Optional[Mapping[str, Any]], state: TState, **kwargs):
58
+ super().__init__(catalog=catalog, config=config, state=state, **{"path_to_yaml": "manifest.yaml"})
59
+
62
60
  @staticmethod
63
61
  def normalize_url(url):
64
62
  parse_result = urlparse(url)
@@ -71,7 +69,7 @@ class SourceGoogleSearchConsole(AbstractSource):
71
69
  authorization = config["authorization"]
72
70
  if authorization["auth_type"] == "Service":
73
71
  try:
74
- authorization["service_account_info"] = json.loads(authorization["service_account_info"])
72
+ json.loads(authorization["service_account_info"])
75
73
  except ValueError:
76
74
  message = "authorization.service_account_info is not valid JSON"
77
75
  raise AirbyteTracedException(message=message, internal_message=message, failure_type=FailureType.config_error)
@@ -113,29 +111,6 @@ class SourceGoogleSearchConsole(AbstractSource):
113
111
  raise AirbyteTracedException(message=message, internal_message=message, failure_type=FailureType.config_error)
114
112
  return config
115
113
 
116
- def check_connection(self, logger: logging.Logger, config: Mapping[str, Any]) -> Tuple[bool, Any]:
117
- try:
118
- config = self._validate_and_transform(config)
119
- stream_kwargs = self.get_stream_kwargs(config)
120
- self.validate_site_urls(config["site_urls"], stream_kwargs["authenticator"])
121
- sites = Sites(**stream_kwargs)
122
- stream_slice = sites.stream_slices(SyncMode.full_refresh)
123
-
124
- # stream_slice returns all site_urls and we need to make sure that
125
- # the connection is successful for all of them
126
- for _slice in stream_slice:
127
- sites_gen = sites.read_records(sync_mode=SyncMode.full_refresh, stream_slice=_slice)
128
- next(sites_gen)
129
- return True, None
130
-
131
- except (InvalidSiteURLValidationError, UnauthorizedOauthError, UnauthorizedServiceAccountError, jsonschema.ValidationError) as e:
132
- return False, repr(e)
133
- except (Exception, UnidentifiedError) as error:
134
- return (
135
- False,
136
- f"Unable to check connectivity to Google Search Console API - {repr(error)}",
137
- )
138
-
139
114
  def validate_site_urls(self, site_urls: List[str], auth: Union[ServiceAccountAuthenticator, Oauth2Authenticator]):
140
115
  if isinstance(auth, ServiceAccountAuthenticator):
141
116
  request = auth(requests.Request(method="GET", url="https://www.googleapis.com/webmasters/v3/sites"))
@@ -154,7 +129,7 @@ class SourceGoogleSearchConsole(AbstractSource):
154
129
  if response.status_code != 200:
155
130
  raise UnidentifiedError(response.json())
156
131
 
157
- remote_site_urls = {s["siteUrl"] for s in response.json()["siteEntry"]}
132
+ remote_site_urls = {s["siteUrl"] for s in response.json().get("siteEntry", [])}
158
133
  invalid_site_url = set(site_urls) - remote_site_urls
159
134
  if invalid_site_url:
160
135
  raise InvalidSiteURLValidationError(invalid_site_url)
@@ -175,25 +150,23 @@ class SourceGoogleSearchConsole(AbstractSource):
175
150
  config = self._validate_and_transform(config)
176
151
  stream_config = self.get_stream_kwargs(config)
177
152
 
178
- streams = [
179
- Sites(**stream_config),
180
- Sitemaps(**stream_config),
181
- SearchAnalyticsByCountry(**stream_config),
182
- SearchAnalyticsByDevice(**stream_config),
183
- SearchAnalyticsByDate(**stream_config),
184
- SearchAnalyticsByQuery(**stream_config),
185
- SearchAnalyticsByPage(**stream_config),
186
- SearchAnalyticsAllFields(**stream_config),
187
- SearchAnalyticsKeywordPageReport(**stream_config),
188
- SearchAnalyticsKeywordPageReportMinimalDimensions(**stream_config),
189
- SearchAnalyticsPageReport(**stream_config),
190
- SearchAnalyticsSiteReportBySite(**stream_config),
191
- SearchAnalyticsSiteReportByPage(**stream_config),
192
- SearchAnalyticsKeywordSiteReportByPage(**stream_config),
193
- SearchAnalyticsKeywordSiteReportByPageMinimalDimensions(**stream_config),
194
- SearchAnalyticsKeywordSiteReportBySite(**stream_config),
195
- SearchAnalyticsKeywordSiteReportBySiteMinimalDimensions(**stream_config),
196
- ]
153
+ streams = super().streams(config=config)
154
+
155
+ streams.extend(
156
+ [
157
+ SearchAnalyticsByDevice(**stream_config),
158
+ SearchAnalyticsByDate(**stream_config),
159
+ SearchAnalyticsByQuery(**stream_config),
160
+ SearchAnalyticsByPage(**stream_config),
161
+ SearchAnalyticsAllFields(**stream_config),
162
+ SearchAnalyticsKeywordPageReport(**stream_config),
163
+ SearchAnalyticsPageReport(**stream_config),
164
+ SearchAnalyticsSiteReportBySite(**stream_config),
165
+ SearchAnalyticsSiteReportByPage(**stream_config),
166
+ SearchAnalyticsKeywordSiteReportByPage(**stream_config),
167
+ SearchAnalyticsKeywordSiteReportBySite(**stream_config),
168
+ ]
169
+ )
197
170
 
198
171
  streams = streams + self.get_custom_reports(config=config, stream_config=stream_config)
199
172
 
@@ -81,40 +81,7 @@ class GoogleSearchConsole(HttpStream, ABC):
81
81
  self.logger.error(f"Stream `{self.name}`. {error.get('message')}. Trying with `aggregationType = auto` instead.")
82
82
  self.aggregation_type = QueryAggregationType.auto
83
83
  setattr(self, "raise_on_http_errors", False)
84
- return super().should_retry(response)
85
-
86
-
87
- class Sites(GoogleSearchConsole):
88
- """
89
- API docs: https://developers.google.com/webmaster-tools/search-console-api-original/v3/sites
90
- """
91
-
92
- primary_key = None
93
-
94
- def path(
95
- self,
96
- stream_state: Mapping[str, Any] = None,
97
- stream_slice: Mapping[str, Any] = None,
98
- next_page_token: Mapping[str, Any] = None,
99
- ) -> str:
100
- return f"sites/{stream_slice.get('site_url')}"
101
-
102
-
103
- class Sitemaps(GoogleSearchConsole):
104
- """
105
- API docs: https://developers.google.com/webmaster-tools/search-console-api-original/v3/sitemaps
106
- """
107
-
108
- primary_key = None
109
- data_field = "sitemap"
110
-
111
- def path(
112
- self,
113
- stream_state: Mapping[str, Any] = None,
114
- stream_slice: Mapping[str, Any] = None,
115
- next_page_token: Mapping[str, Any] = None,
116
- ) -> str:
117
- return f"sites/{stream_slice.get('site_url')}/sitemaps"
84
+ return response.status_code == 429 or 500 <= response.status_code < 600
118
85
 
119
86
 
120
87
  class SearchAnalytics(GoogleSearchConsole, CheckpointMixin, ABC):
@@ -333,12 +300,6 @@ class SearchAnalyticsByDate(SearchAnalytics):
333
300
  dimensions = ["date"]
334
301
 
335
302
 
336
- class SearchAnalyticsByCountry(SearchAnalytics):
337
- primary_key = ["site_url", "date", "country", "search_type"]
338
- search_types = ["web", "news", "image", "video", "discover", "googleNews"]
339
- dimensions = ["date", "country"]
340
-
341
-
342
303
  class SearchAnalyticsByDevice(SearchAnalytics):
343
304
  primary_key = ["site_url", "date", "device", "search_type"]
344
305
  search_types = ["web", "news", "image", "video", "googleNews"]
@@ -384,7 +345,7 @@ class SearchByKeyword(SearchAnalytics):
384
345
  def stream_slices(
385
346
  self, sync_mode: SyncMode, cursor_field: List[str] = None, stream_state: Mapping[str, Any] = None
386
347
  ) -> Iterable[Optional[Mapping[str, Any]]]:
387
- search_appearance_stream = SearchAppearance(self._session.auth, self._site_urls, self._start_date, self._end_date)
348
+ search_appearance_stream = SearchAppearance(self._http_client._session.auth, self._site_urls, self._start_date, self._end_date)
388
349
 
389
350
  for stream_slice in super().stream_slices(sync_mode, cursor_field, stream_state):
390
351
  keywords_records = search_appearance_stream.read_records(
@@ -395,6 +356,7 @@ class SearchByKeyword(SearchAnalytics):
395
356
  for keyword in keywords:
396
357
  filters = {"dimension": "searchAppearance", "operator": "equals", "expression": keyword}
397
358
  stream_slice["dimensionFilterGroups"] = [{"groupType": "and", "filters": filters}]
359
+
398
360
  yield stream_slice
399
361
 
400
362
  def request_body_json(
@@ -413,35 +375,18 @@ class SearchAnalyticsKeywordPageReport(SearchByKeyword):
413
375
  dimensions = ["date", "country", "device", "query", "page"]
414
376
 
415
377
 
416
- class SearchAnalyticsKeywordPageReportMinimalDimensions(SearchByKeyword):
417
- primary_key = ["site_url", "date", "country", "device", "query", "page", "search_type"]
418
- dimensions = ["date"]
419
-
420
-
421
378
  class SearchAnalyticsKeywordSiteReportByPage(SearchByKeyword):
422
379
  primary_key = ["site_url", "date", "country", "device", "query", "search_type"]
423
380
  dimensions = ["date", "country", "device", "query"]
424
381
  aggregation_type = QueryAggregationType.by_page
425
382
 
426
383
 
427
- class SearchAnalyticsKeywordSiteReportByPageMinimalDimensions(SearchByKeyword):
428
- primary_key = ["site_url", "date", "country", "device", "query", "search_type"]
429
- dimensions = ["date"]
430
- aggregation_type = QueryAggregationType.by_page
431
-
432
-
433
384
  class SearchAnalyticsKeywordSiteReportBySite(SearchByKeyword):
434
385
  primary_key = ["site_url", "date", "country", "device", "query", "search_type"]
435
386
  dimensions = ["date", "country", "device", "query"]
436
387
  aggregation_type = QueryAggregationType.by_property
437
388
 
438
389
 
439
- class SearchAnalyticsKeywordSiteReportBySiteMinimalDimensions(SearchByKeyword):
440
- primary_key = ["site_url", "date", "country", "device", "query", "search_type"]
441
- dimensions = ["date"]
442
- aggregation_type = QueryAggregationType.by_property
443
-
444
-
445
390
  class SearchAnalyticsSiteReportBySite(SearchAnalytics):
446
391
  primary_key = ["site_url", "date", "country", "device", "search_type"]
447
392
  dimensions = ["date", "country", "device"]
@@ -1,41 +0,0 @@
1
- {
2
- "$schema": "http://json-schema.org/draft-07/schema#",
3
- "type": "object",
4
- "properties": {
5
- "site_url": {
6
- "description": "The URL of the site for which the search analytics data is being reported.",
7
- "type": ["null", "string"]
8
- },
9
- "search_type": {
10
- "description": "The type of search (web search, image search, video search, etc.) for which the data is being reported.",
11
- "type": ["null", "string"]
12
- },
13
- "date": {
14
- "description": "The date for which the search analytics data is being reported.",
15
- "type": ["null", "string"],
16
- "format": "date"
17
- },
18
- "country": {
19
- "description": "The country for which the search analytics data is being reported.",
20
- "type": ["null", "string"]
21
- },
22
- "clicks": {
23
- "description": "The number of times users clicked on the search result for a specific country.",
24
- "type": ["null", "integer"]
25
- },
26
- "impressions": {
27
- "description": "The total number of times a search result was shown in search results for a specific country.",
28
- "type": ["null", "integer"]
29
- },
30
- "ctr": {
31
- "description": "The click-through rate, i.e., the ratio of clicks to impressions for a specific country.",
32
- "type": ["null", "number"],
33
- "multipleOf": 1e-25
34
- },
35
- "position": {
36
- "description": "The average position at which the site's search result appeared for a specific country.",
37
- "type": ["null", "number"],
38
- "multipleOf": 1e-25
39
- }
40
- }
41
- }
@@ -1,54 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft-07/schema#",
3
- "type": "object",
4
- "additionalProperties": true,
5
- "properties": {
6
- "site_url": {
7
- "description": "The URL of the website being monitored.",
8
- "type": ["null", "string"]
9
- },
10
- "search_type": {
11
- "description": "The type of search (e.g., web, image, video).",
12
- "type": ["null", "string"]
13
- },
14
- "date": {
15
- "description": "The date of the search data collected.",
16
- "type": ["null", "string"],
17
- "format": "date"
18
- },
19
- "country": {
20
- "description": "The country where the search is made.",
21
- "type": ["null", "string"]
22
- },
23
- "device": {
24
- "description": "The device type used for the search (e.g., desktop, mobile).",
25
- "type": ["null", "string"]
26
- },
27
- "page": {
28
- "description": "The page URL on which the keyword appears in search results.",
29
- "type": ["null", "string"]
30
- },
31
- "query": {
32
- "description": "The search query used to find the site.",
33
- "type": ["null", "string"]
34
- },
35
- "clicks": {
36
- "description": "The number of clicks for the keyword on a specific page.",
37
- "type": ["null", "integer"]
38
- },
39
- "impressions": {
40
- "description": "The number of times the keyword appeared in search results.",
41
- "type": ["null", "integer"]
42
- },
43
- "ctr": {
44
- "description": "Click-through rate which is the percentage of clicks divided by impressions.",
45
- "type": ["null", "number"],
46
- "multipleOf": 1e-25
47
- },
48
- "position": {
49
- "description": "The average position of the keyword on search results pages.",
50
- "type": ["null", "number"],
51
- "multipleOf": 1e-25
52
- }
53
- }
54
- }
@@ -1,50 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft-07/schema#",
3
- "type": "object",
4
- "additionalProperties": true,
5
- "properties": {
6
- "site_url": {
7
- "description": "The URL of the website for which the search analytics data is retrieved.",
8
- "type": ["null", "string"]
9
- },
10
- "search_type": {
11
- "description": "The type of search conducted (e.g., web, image, video).",
12
- "type": ["null", "string"]
13
- },
14
- "date": {
15
- "description": "The date when the search data was recorded.",
16
- "type": ["null", "string"],
17
- "format": "date"
18
- },
19
- "country": {
20
- "description": "The country from which the search query originated.",
21
- "type": ["null", "string"]
22
- },
23
- "device": {
24
- "description": "The device type used for the search query (e.g., desktop, mobile).",
25
- "type": ["null", "string"]
26
- },
27
- "query": {
28
- "description": "The search query used by the user.",
29
- "type": ["null", "string"]
30
- },
31
- "clicks": {
32
- "description": "The number of times users clicked on your website link in search results.",
33
- "type": ["null", "integer"]
34
- },
35
- "impressions": {
36
- "description": "The number of times your website link appeared in search results.",
37
- "type": ["null", "integer"]
38
- },
39
- "ctr": {
40
- "description": "Click-through rate: Number of clicks divided by the number of impressions.",
41
- "type": ["null", "number"],
42
- "multipleOf": 1e-25
43
- },
44
- "position": {
45
- "description": "The average position of your website link in search results.",
46
- "type": ["null", "number"],
47
- "multipleOf": 1e-25
48
- }
49
- }
50
- }
@@ -1,50 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft-07/schema#",
3
- "type": "object",
4
- "additionalProperties": true,
5
- "properties": {
6
- "site_url": {
7
- "description": "The URL of the site for which the search analytics data is recorded.",
8
- "type": ["null", "string"]
9
- },
10
- "search_type": {
11
- "description": "The type of search (e.g., web search, image search) that generated the analytics data.",
12
- "type": ["null", "string"]
13
- },
14
- "date": {
15
- "description": "The date for which the search analytics data is recorded.",
16
- "type": ["null", "string"],
17
- "format": "date"
18
- },
19
- "country": {
20
- "description": "The country from which the search originated.",
21
- "type": ["null", "string"]
22
- },
23
- "device": {
24
- "description": "The type of device used by the user during the search (e.g., desktop, mobile).",
25
- "type": ["null", "string"]
26
- },
27
- "query": {
28
- "description": "The search query used by the user to find the site in search results.",
29
- "type": ["null", "string"]
30
- },
31
- "clicks": {
32
- "description": "The number of times users clicked on the search result linking to the site.",
33
- "type": ["null", "integer"]
34
- },
35
- "impressions": {
36
- "description": "The number of times the site was shown in search results to users.",
37
- "type": ["null", "integer"]
38
- },
39
- "ctr": {
40
- "description": "Click-through rate represents the percentage of users who clicked on the site's link after seeing it in search results.",
41
- "type": ["null", "number"],
42
- "multipleOf": 1e-25
43
- },
44
- "position": {
45
- "description": "The average ranking position of the site in search results.",
46
- "type": ["null", "number"],
47
- "multipleOf": 1e-25
48
- }
49
- }
50
- }
@@ -1,61 +0,0 @@
1
- {
2
- "$schema": "http://json-schema.org/draft-07/schema#",
3
- "type": "object",
4
- "properties": {
5
- "path": {
6
- "description": "Path to the sitemap file",
7
- "type": ["null", "string"]
8
- },
9
- "lastSubmitted": {
10
- "description": "Timestamp when the sitemap was last submitted",
11
- "type": ["null", "string"],
12
- "format": "date-time"
13
- },
14
- "isPending": {
15
- "description": "Flag indicating if the sitemap is pending for processing",
16
- "type": ["null", "boolean"]
17
- },
18
- "isSitemapsIndex": {
19
- "description": "Flag indicating if the data represents a sitemap index",
20
- "type": ["null", "boolean"]
21
- },
22
- "type": {
23
- "description": "Type of the sitemap",
24
- "type": ["null", "string"]
25
- },
26
- "lastDownloaded": {
27
- "description": "Timestamp when the sitemap was last downloaded",
28
- "type": ["null", "string"],
29
- "format": "date-time"
30
- },
31
- "warnings": {
32
- "description": "Warnings encountered while processing the sitemaps",
33
- "type": ["null", "string"]
34
- },
35
- "errors": {
36
- "description": "Errors encountered while processing the sitemaps",
37
- "type": ["null", "string"]
38
- },
39
- "contents": {
40
- "description": "Data related to the sitemap contents",
41
- "type": "array",
42
- "items": {
43
- "type": "object",
44
- "properties": {
45
- "type": {
46
- "description": "Type of the sitemap content",
47
- "type": ["null", "string"]
48
- },
49
- "submitted": {
50
- "description": "Number of submitted sitemap URLs",
51
- "type": ["null", "string"]
52
- },
53
- "indexed": {
54
- "description": "Number of indexed sitemap URLs",
55
- "type": ["null", "string"]
56
- }
57
- }
58
- }
59
- }
60
- }
61
- }
@@ -1,14 +0,0 @@
1
- {
2
- "$schema": "http://json-schema.org/draft-07/schema#",
3
- "type": "object",
4
- "properties": {
5
- "siteUrl": {
6
- "description": "The URL of the site data being fetched",
7
- "type": ["null", "string"]
8
- },
9
- "permissionLevel": {
10
- "description": "The user's permission level for the site (owner, full, restricted, etc.)",
11
- "type": ["null", "string"]
12
- }
13
- }
14
- }