| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 90c020d commit dc01c59
3 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2234,34 +2234,104 @@ def make_private(self, client=None): | |||
| 2234 | 2234 | self.acl.all().revoke_read() | |
| 2235 | 2235 | self.acl.save(client=client) | |
| 2236 | 2236 | ||
| 2237 | - def compose(self, sources, client=None, timeout=_DEFAULT_TIMEOUT): | ||
| 2237 | + def compose( | ||
| 2238 | + self, | ||
| 2239 | + sources, | ||
| 2240 | + client=None, | ||
| 2241 | + timeout=_DEFAULT_TIMEOUT, | ||
| 2242 | + if_generation_match=None, | ||
| 2243 | + if_metageneration_match=None, | ||
| 2244 | + ): | ||
| 2238 | 2245 | """Concatenate source blobs into this one. | |
| 2239 | 2246 | ||
| 2240 | 2247 | If :attr:`user_project` is set on the bucket, bills the API request | |
| 2241 | 2248 | to that project. | |
| 2242 | 2249 | ||
| 2243 | 2250 | :type sources: list of :class:`Blob` | |
| 2244 | - :param sources: blobs whose contents will be composed into this blob. | ||
| 2251 | + :param sources: Blobs whose contents will be composed into this blob. | ||
| 2245 | 2252 | ||
| 2246 | 2253 | :type client: :class:`~google.cloud.storage.client.Client` or | |
| 2247 | 2254 | ``NoneType`` | |
| 2248 | - :param client: (Optional) The client to use. If not passed, falls back | ||
| 2255 | + :param client: (Optional) The client to use. If not passed, falls back | ||
| 2249 | 2256 | to the ``client`` stored on the blob's bucket. | |
| 2257 | + | ||
| 2250 | 2258 | :type timeout: float or tuple | |
| 2251 | 2259 | :param timeout: (Optional) The amount of time, in seconds, to wait | |
| 2252 | 2260 | for the server response. | |
| 2253 | 2261 | ||
| 2254 | 2262 | Can also be passed as a tuple (connect_timeout, read_timeout). | |
| 2255 | 2263 | See :meth:`requests.Session.request` documentation for details. | |
| 2264 | + | ||
| 2265 | + :type if_generation_match: list of long | ||
| 2266 | + :param if_generation_match: (Optional) Make the operation conditional on whether | ||
| 2267 | + the blob's current generation matches the given value. | ||
| 2268 | + Setting to 0 makes the operation succeed only if there | ||
| 2269 | + are no live versions of the blob. The list must match | ||
| 2270 | + ``sources`` item-to-item. | ||
| 2271 | + | ||
| 2272 | + :type if_metageneration_match: list of long | ||
| 2273 | + :param if_metageneration_match: (Optional) Make the operation conditional on whether | ||
| 2274 | + the blob's current metageneration matches the given | ||
| 2275 | + value. The list must match ``sources`` item-to-item. | ||
| 2276 | + | ||
| 2277 | + Example: | ||
| 2278 | + Compose blobs using generation match preconditions. | ||
| 2279 | + | ||
| 2280 | + >>> from google.cloud import storage | ||
| 2281 | + >>> client = storage.Client() | ||
| 2282 | + >>> bucket = client.bucket("bucket-name") | ||
| 2283 | + | ||
| 2284 | + >>> blobs = [bucket.blob("blob-name-1"), bucket.blob("blob-name-2")] | ||
| 2285 | + >>> if_generation_match = [None] * len(blobs) | ||
| 2286 | + >>> if_generation_match[0] = "123" # precondition for "blob-name-1" | ||
| 2287 | + | ||
| 2288 | + >>> composed_blob = bucket.blob("composed-name") | ||
| 2289 | + >>> composed_blob.compose(blobs, if_generation_match) | ||
| 2256 | 2290 | """ | |
| 2291 | + sources_len = len(sources) | ||
| 2292 | + if if_generation_match is not None and len(if_generation_match) != sources_len: | ||
| 2293 | + raise ValueError( | ||
| 2294 | + "'if_generation_match' length must be the same as 'sources' length" | ||
| 2295 | + ) | ||
| 2296 | + | ||
| 2297 | + if ( | ||
| 2298 | + if_metageneration_match is not None | ||
| 2299 | + and len(if_metageneration_match) != sources_len | ||
| 2300 | + ): | ||
| 2301 | + raise ValueError( | ||
| 2302 | + "'if_metageneration_match' length must be the same as 'sources' length" | ||
| 2303 | + ) | ||
| 2304 | + | ||
| 2257 | 2305 | client = self._require_client(client) | |
| 2258 | 2306 | query_params = {} | |
| 2259 | 2307 | ||
| 2260 | 2308 | if self.user_project is not None: | |
| 2261 | 2309 | query_params["userProject"] = self.user_project | |
| 2262 | 2310 | ||
| 2311 | + source_objects = [] | ||
| 2312 | + for index, source in enumerate(sources): | ||
| 2313 | + source_object = {"name": source.name} | ||
| 2314 | + | ||
| 2315 | + preconditions = {} | ||
| 2316 | + if ( | ||
| 2317 | + if_generation_match is not None | ||
| 2318 | + and if_generation_match[index] is not None | ||
| 2319 | + ): | ||
| 2320 | + preconditions["ifGenerationMatch"] = if_generation_match[index] | ||
| 2321 | + | ||
| 2322 | + if ( | ||
| 2323 | + if_metageneration_match is not None | ||
| 2324 | + and if_metageneration_match[index] is not None | ||
| 2325 | + ): | ||
| 2326 | + preconditions["ifMetagenerationMatch"] = if_metageneration_match[index] | ||
| 2327 | + | ||
| 2328 | + if preconditions: | ||
| 2329 | + source_object["objectPreconditions"] = preconditions | ||
| 2330 | + | ||
| 2331 | + source_objects.append(source_object) | ||
| 2332 | + | ||
| 2263 | 2333 | request = { | |
| 2264 | - "sourceObjects": [{"name": source.name} for source in sources], | ||
| 2334 | + "sourceObjects": source_objects, | ||
| 2265 | 2335 | "destination": self._properties.copy(), | |
| 2266 | 2336 | } | |
| 2267 | 2337 | api_response = client._connection.api_request( | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1441,6 +1441,34 @@ def test_compose_replace_existing_blob(self): | |||
| 1441 | 1441 | composed = original.download_as_string() | |
| 1442 | 1442 | self.assertEqual(composed, BEFORE + TO_APPEND) | |
| 1443 | 1443 | ||
| 1444 | + def test_compose_with_generation_match(self): | ||
| 1445 | + BEFORE = b"AAA\n" | ||
| 1446 | + original = self.bucket.blob("original") | ||
| 1447 | + original.content_type = "text/plain" | ||
| 1448 | + original.upload_from_string(BEFORE) | ||
| 1449 | + self.case_blobs_to_delete.append(original) | ||
| 1450 | + | ||
| 1451 | + TO_APPEND = b"BBB\n" | ||
| 1452 | + to_append = self.bucket.blob("to_append") | ||
| 1453 | + to_append.upload_from_string(TO_APPEND) | ||
| 1454 | + self.case_blobs_to_delete.append(to_append) | ||
| 1455 | + | ||
| 1456 | + with self.assertRaises(google.api_core.exceptions.PreconditionFailed): | ||
| 1457 | + original.compose( | ||
| 1458 | + [original, to_append], | ||
| 1459 | + if_generation_match=[6, 7], | ||
| 1460 | + if_metageneration_match=[8, 9], | ||
| 1461 | + ) | ||
| 1462 | + | ||
| 1463 | + original.compose( | ||
| 1464 | + [original, to_append], | ||
| 1465 | + if_generation_match=[original.generation, to_append.generation], | ||
| 1466 | + if_metageneration_match=[original.metageneration, to_append.metageneration], | ||
| 1467 | + ) | ||
| 1468 | + | ||
| 1469 | + composed = original.download_as_string() | ||
| 1470 | + self.assertEqual(composed, BEFORE + TO_APPEND) | ||
| 1471 | + | ||
| 1444 | 1472 | @unittest.skipUnless(USER_PROJECT, "USER_PROJECT not set in environment.") | |
| 1445 | 1473 | def test_compose_with_user_project(self): | |
| 1446 | 1474 | new_bucket_name = "compose-user-project" + unique_resource_id("-") | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2676,7 +2676,7 @@ def test_make_private(self): | |||
| 2676 | 2676 | def test_compose_wo_content_type_set(self): | |
| 2677 | 2677 | SOURCE_1 = "source-1" | |
| 2678 | 2678 | SOURCE_2 = "source-2" | |
| 2679 | - DESTINATION = "destinaton" | ||
| 2679 | + DESTINATION = "destination" | ||
| 2680 | 2680 | RESOURCE = {} | |
| 2681 | 2681 | after = ({"status": http_client.OK}, RESOURCE) | |
| 2682 | 2682 | connection = _Connection(after) | |
@@ -2711,7 +2711,7 @@ def test_compose_wo_content_type_set(self): | |||
| 2711 | 2711 | def test_compose_minimal_w_user_project(self): | |
| 2712 | 2712 | SOURCE_1 = "source-1" | |
| 2713 | 2713 | SOURCE_2 = "source-2" | |
| 2714 | - DESTINATION = "destinaton" | ||
| 2714 | + DESTINATION = "destination" | ||
| 2715 | 2715 | RESOURCE = {"etag": "DEADBEEF"} | |
| 2716 | 2716 | USER_PROJECT = "user-project-123" | |
| 2717 | 2717 | after = ({"status": http_client.OK}, RESOURCE) | |
@@ -2747,7 +2747,7 @@ def test_compose_minimal_w_user_project(self): | |||
| 2747 | 2747 | def test_compose_w_additional_property_changes(self): | |
| 2748 | 2748 | SOURCE_1 = "source-1" | |
| 2749 | 2749 | SOURCE_2 = "source-2" | |
| 2750 | - DESTINATION = "destinaton" | ||
| 2750 | + DESTINATION = "destination" | ||
| 2751 | 2751 | RESOURCE = {"etag": "DEADBEEF"} | |
| 2752 | 2752 | after = ({"status": http_client.OK}, RESOURCE) | |
| 2753 | 2753 | connection = _Connection(after) | |
@@ -2785,6 +2785,129 @@ def test_compose_w_additional_property_changes(self): | |||
| 2785 | 2785 | }, | |
| 2786 | 2786 | ) | |
| 2787 | 2787 | ||
| 2788 | + def test_compose_w_generation_match(self): | ||
| 2789 | + SOURCE_1 = "source-1" | ||
| 2790 | + SOURCE_2 = "source-2" | ||
| 2791 | + DESTINATION = "destination" | ||
| 2792 | + RESOURCE = {} | ||
| 2793 | + GENERATION_NUMBERS = [6, 9] | ||
| 2794 | + METAGENERATION_NUMBERS = [7, 1] | ||
| 2795 | + | ||
| 2796 | + after = ({"status": http_client.OK}, RESOURCE) | ||
| 2797 | + connection = _Connection(after) | ||
| 2798 | + client = _Client(connection) | ||
| 2799 | + bucket = _Bucket(client=client) | ||
| 2800 | + source_1 = self._make_one(SOURCE_1, bucket=bucket) | ||
| 2801 | + source_2 = self._make_one(SOURCE_2, bucket=bucket) | ||
| 2802 | + | ||
| 2803 | + destination = self._make_one(DESTINATION, bucket=bucket) | ||
| 2804 | + destination.compose( | ||
| 2805 | + sources=[source_1, source_2], | ||
| 2806 | + if_generation_match=GENERATION_NUMBERS, | ||
| 2807 | + if_metageneration_match=METAGENERATION_NUMBERS, | ||
| 2808 | + ) | ||
| 2809 | + | ||
| 2810 | + kw = connection._requested | ||
| 2811 | + self.assertEqual(len(kw), 1) | ||
| 2812 | + self.assertEqual( | ||
| 2813 | + kw[0], | ||
| 2814 | + { | ||
| 2815 | + "method": "POST", | ||
| 2816 | + "path": "/b/name/o/%s/compose" % DESTINATION, | ||
| 2817 | + "query_params": {}, | ||
| 2818 | + "data": { | ||
| 2819 | + "sourceObjects": [ | ||
| 2820 | + { | ||
| 2821 | + "name": source_1.name, | ||
| 2822 | + "objectPreconditions": { | ||
| 2823 | + "ifGenerationMatch": GENERATION_NUMBERS[0], | ||
| 2824 | + "ifMetagenerationMatch": METAGENERATION_NUMBERS[0], | ||
| 2825 | + }, | ||
| 2826 | + }, | ||
| 2827 | + { | ||
| 2828 | + "name": source_2.name, | ||
| 2829 | + "objectPreconditions": { | ||
| 2830 | + "ifGenerationMatch": GENERATION_NUMBERS[1], | ||
| 2831 | + "ifMetagenerationMatch": METAGENERATION_NUMBERS[1], | ||
| 2832 | + }, | ||
| 2833 | + }, | ||
| 2834 | + ], | ||
| 2835 | + "destination": {}, | ||
| 2836 | + }, | ||
| 2837 | + "_target_object": destination, | ||
| 2838 | + "timeout": self._get_default_timeout(), | ||
| 2839 | + }, | ||
| 2840 | + ) | ||
| 2841 | + | ||
| 2842 | + def test_compose_w_generation_match_bad_length(self): | ||
| 2843 | + SOURCE_1 = "source-1" | ||
| 2844 | + SOURCE_2 = "source-2" | ||
| 2845 | + DESTINATION = "destination" | ||
| 2846 | + GENERATION_NUMBERS = [6] | ||
| 2847 | + METAGENERATION_NUMBERS = [7] | ||
| 2848 | + | ||
| 2849 | + after = ({"status": http_client.OK}, {}) | ||
| 2850 | + connection = _Connection(after) | ||
| 2851 | + client = _Client(connection) | ||
| 2852 | + bucket = _Bucket(client=client) | ||
| 2853 | + source_1 = self._make_one(SOURCE_1, bucket=bucket) | ||
| 2854 | + source_2 = self._make_one(SOURCE_2, bucket=bucket) | ||
| 2855 | + | ||
| 2856 | + destination = self._make_one(DESTINATION, bucket=bucket) | ||
| 2857 | + | ||
| 2858 | + with self.assertRaises(ValueError): | ||
| 2859 | + destination.compose( | ||
| 2860 | + sources=[source_1, source_2], if_generation_match=GENERATION_NUMBERS, | ||
| 2861 | + ) | ||
| 2862 | + with self.assertRaises(ValueError): | ||
| 2863 | + destination.compose( | ||
| 2864 | + sources=[source_1, source_2], | ||
| 2865 | + if_metageneration_match=METAGENERATION_NUMBERS, | ||
| 2866 | + ) | ||
| 2867 | + | ||
| 2868 | + def test_compose_w_generation_match_nones(self): | ||
| 2869 | + SOURCE_1 = "source-1" | ||
| 2870 | + SOURCE_2 = "source-2" | ||
| 2871 | + DESTINATION = "destination" | ||
| 2872 | + GENERATION_NUMBERS = [6, None] | ||
| 2873 | + | ||
| 2874 | + after = ({"status": http_client.OK}, {}) | ||
| 2875 | + connection = _Connection(after) | ||
| 2876 | + client = _Client(connection) | ||
| 2877 | + bucket = _Bucket(client=client) | ||
| 2878 | + source_1 = self._make_one(SOURCE_1, bucket=bucket) | ||
| 2879 | + source_2 = self._make_one(SOURCE_2, bucket=bucket) | ||
| 2880 | + | ||
| 2881 | + destination = self._make_one(DESTINATION, bucket=bucket) | ||
| 2882 | + destination.compose( | ||
| 2883 | + sources=[source_1, source_2], if_generation_match=GENERATION_NUMBERS, | ||
| 2884 | + ) | ||
| 2885 | + | ||
| 2886 | + kw = connection._requested | ||
| 2887 | + self.assertEqual(len(kw), 1) | ||
| 2888 | + self.assertEqual( | ||
| 2889 | + kw[0], | ||
| 2890 | + { | ||
| 2891 | + "method": "POST", | ||
| 2892 | + "path": "/b/name/o/%s/compose" % DESTINATION, | ||
| 2893 | + "query_params": {}, | ||
| 2894 | + "data": { | ||
| 2895 | + "sourceObjects": [ | ||
| 2896 | + { | ||
| 2897 | + "name": source_1.name, | ||
| 2898 | + "objectPreconditions": { | ||
| 2899 | + "ifGenerationMatch": GENERATION_NUMBERS[0], | ||
| 2900 | + }, | ||
| 2901 | + }, | ||
| 2902 | + {"name": source_2.name}, | ||
| 2903 | + ], | ||
| 2904 | + "destination": {}, | ||
| 2905 | + }, | ||
| 2906 | + "_target_object": destination, | ||
| 2907 | + "timeout": self._get_default_timeout(), | ||
| 2908 | + }, | ||
| 2909 | + ) | ||
| 2910 | + | ||
| 2788 | 2911 | def test_rewrite_response_without_resource(self): | |
| 2789 | 2912 | SOURCE_BLOB = "source" | |
| 2790 | 2913 | DEST_BLOB = "dest" | |
| Back | FazBrowse Home | New Git URL |
0 commit comments