Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ML-3730: fix support for client running on windows OS #106

Open
wants to merge 5 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ def test_verifier_transport(self):

# verify some stuff from the request
self.assertEqual(request.container, container_name)
self.assertEqual(request.path, os.path.join(os.sep, container_name, 'some/path'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want to put this in the readme? If so, we should put the import there as well.

self.assertEqual(request.path, url_join(os.sep, container_name, 'some/path'))

# return a mocked response
return unittest.mock.MagicMock(status_code=200,
Expand All @@ -276,7 +276,7 @@ def test_verifier_transport(self):

# verify some stuff from the request
self.assertEqual(request.container, container_name)
self.assertEqual(request.path, os.path.join(os.sep, container_name, 'some/table/path/some_item_key'))
self.assertEqual(request.path, url_join(os.sep, container_name, 'some/table/path/some_item_key'))

# prepare and output mock
output = unittest.mock.MagicMock(item={
Expand Down
14 changes: 14 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2023 Iguazio
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
30 changes: 15 additions & 15 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@

import future.utils

import v3io.common.helpers
import v3io.dataplane
import v3io.dataplane.output
import v3io.dataplane.response
import v3io.logger
from v3io.common.helpers import url_join


class Test(unittest.TestCase):
Expand Down Expand Up @@ -64,7 +64,7 @@ def _delete_dir(self, path):
class TestContainer(Test):
def setUp(self):
super(TestContainer, self).setUp()
self._path = os.path.join(self._test_parent_dir, "v3io-py-test-container")
self._path = url_join(self._test_parent_dir, "v3io-py-test-container")

# clean up
self._delete_dir(self._path)
Expand All @@ -84,13 +84,13 @@ def test_get_container_contents_valid(self):
for object_index in range(file_number):
self._client.object.put(
container=self._container,
path=os.path.join(self._path, "object-{0}.txt".format(object_index)),
path=url_join(self._path, "object-{0}.txt".format(object_index)),
body=body,
)

for object_index in range(dir_number):
self._client.object.put(
container=self._container, path=os.path.join(self._path, "dir-{0}/".format(object_index))
container=self._container, path=url_join(self._path, "dir-{0}/".format(object_index))
)

response = self._client.container.list(
Expand All @@ -111,7 +111,7 @@ class TestStream(Test):
def setUp(self):
super(TestStream, self).setUp()

self._path = os.path.join(self._test_parent_dir, "v3io-py-test-stream")
self._path = url_join(self._test_parent_dir, "v3io-py-test-stream")

# clean up
self._client.stream.delete(container=self._container, stream_path=self._path, raise_for_status=[200, 204, 404])
Expand All @@ -136,7 +136,7 @@ def test_delete_stream_with_cg(self):
# write several "consumer group state" files
for cg_id in range(3):
self._client.object.put(
container=self._container, path=os.path.join(self._path, "cg{}-state.json".format(cg_id))
container=self._container, path=url_join(self._path, "cg{}-state.json".format(cg_id))
)

# check that the stream doesn't exist
Expand Down Expand Up @@ -209,7 +209,7 @@ class TestObject(Test):
def setUp(self):
super(TestObject, self).setUp()

self._object_dir = os.path.join(self._test_parent_dir, "v3io-py-test-object")
self._object_dir = url_join(self._test_parent_dir, "v3io-py-test-object")
self._object_path = self._object_dir + "/obj ect.txt"

# clean up
Expand Down Expand Up @@ -315,8 +315,8 @@ class TestSchema(Test):
def setUp(self):
super(TestSchema, self).setUp()

self._schema_dir = os.path.join(self._test_parent_dir, "v3io-py-test-schema")
self._schema_path = os.path.join(self._schema_dir, ".#schema")
self._schema_dir = url_join(self._test_parent_dir, "v3io-py-test-schema")
self._schema_path = url_join(self._schema_dir, ".#schema")

# clean up
self._delete_dir(self._schema_dir)
Expand Down Expand Up @@ -371,7 +371,7 @@ class TestKv(Test):
def setUp(self):
super(TestKv, self).setUp()

self._path = os.path.join(self._test_parent_dir, "some_dir/v3io-py-test-emd")
self._path = url_join(self._test_parent_dir, "some_dir/v3io-py-test-emd")
self._delete_dir(self._path)

def test_kv_array(self):
Expand Down Expand Up @@ -399,7 +399,7 @@ def test_kv_array(self):

def test_kv_values(self):
def _get_int_array():
int_array = array.array("l")
int_array = array.array("q")
for value in range(10):
int_array.append(value)

Expand Down Expand Up @@ -587,7 +587,7 @@ def _compare_item_values(self, v1, v2):
v1 = list(v1)

if v1 != v2:
self.fail("Values dont match")
self.fail(f"Values don't match: {v1} vs {v2}")

def _compare_item_types(self, v1, v2):
if isinstance(v1, array.array):
Expand Down Expand Up @@ -681,7 +681,7 @@ class TestConnectonErrorRecovery(Test):
def setUp(self):
super(TestConnectonErrorRecovery, self).setUp()

self._object_dir = os.path.join(self._test_parent_dir, "v3io-py-test-connection-error")
self._object_dir = url_join(self._test_parent_dir, "v3io-py-test-connection-error")
self._object_path = self._object_dir + "/object.txt"

self._kv_path = "some_dir/v3io-py-test-emd"
Expand Down Expand Up @@ -761,15 +761,15 @@ def test_verifier_transport(self):
def _verify_object_get(request):
# verify some stuff from the request
self.assertEqual(request.container, container_name)
self.assertEqual(request.path, os.path.join(os.sep, container_name, "some/path"))
self.assertEqual(request.path, url_join("/", container_name, "some/path"))

# return a mocked response
return unittest.mock.MagicMock(status_code=200, body="some body")

def _verify_kv_get(request):
# verify some stuff from the request
self.assertEqual(request.container, container_name)
self.assertEqual(request.path, os.path.join(os.sep, container_name, "some/table/path/some_item_key"))
self.assertEqual(request.path, url_join("/", container_name, "some/table/path/some_item_key"))

# prepare and output mock
output = unittest.mock.MagicMock(item={"some_key": "some_value"})
Expand Down
19 changes: 10 additions & 9 deletions tests/test_client_aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import v3io.aio.dataplane
import v3io.dataplane
from v3io.common.helpers import url_join


class Test(unittest.IsolatedAsyncioTestCase):
Expand Down Expand Up @@ -56,7 +57,7 @@ async def _delete_dir(self, path):
class TestContainer(Test):
async def asyncSetUp(self):
await super(TestContainer, self).asyncSetUp()
self._path = os.path.join(self._test_parent_dir, "v3io-py-test-container")
self._path = url_join(self._test_parent_dir, "v3io-py-test-container")

# clean up
await self._delete_dir(self._path)
Expand All @@ -74,13 +75,13 @@ async def test_get_container_contents(self):
for object_index in range(5):
await self._client.object.put(
container=self._container,
path=os.path.join(self._path, "object-{0}.txt".format(object_index)),
path=url_join(self._path, "object-{0}.txt".format(object_index)),
body=body,
)

for object_index in range(5):
await self._client.object.put(
container=self._container, path=os.path.join(self._path, "dir-{0}/".format(object_index))
container=self._container, path=url_join(self._path, "dir-{0}/".format(object_index))
)

response = await self._client.container.list(
Expand All @@ -103,7 +104,7 @@ class TestStream(Test):
async def asyncSetUp(self):
await super(TestStream, self).asyncSetUp()

self._path = os.path.join(self._test_parent_dir, "v3io-py-test-stream")
self._path = url_join(self._test_parent_dir, "v3io-py-test-stream")

# clean up
await self._client.stream.delete(
Expand All @@ -130,7 +131,7 @@ async def test_delete_stream_with_cg(self):
# write several "consumer group state" files
for cg_id in range(3):
await self._client.object.put(
container=self._container, path=os.path.join(self._path, "cg{}-state.json".format(cg_id))
container=self._container, path=url_join(self._path, "cg{}-state.json".format(cg_id))
)

# check that the stream doesn't exist
Expand Down Expand Up @@ -207,7 +208,7 @@ class TestObject(Test):
async def asyncSetUp(self):
await super(TestObject, self).asyncSetUp()

self._object_dir = os.path.join(self._test_parent_dir, "v3io-py-test-object")
self._object_dir = url_join(self._test_parent_dir, "v3io-py-test-object")
self._object_path = self._object_dir + "/obj ect.txt"

# clean up
Expand Down Expand Up @@ -290,7 +291,7 @@ async def test_get_offset(self):
# await super(TestSchema, self).asyncSetUp()
#
# self._schema_dir = "/v3io-py-test-schemaa"
# self._schema_path = os.path.join(self._schema_dir, ".%23schema")
# self._schema_path = url_join(self._schema_dir, ".%23schema")
#
# # clean up
# await self._delete_dir(self._schema_dir)
Expand Down Expand Up @@ -346,7 +347,7 @@ class TestKv(Test):
async def asyncSetUp(self):
await super(TestKv, self).asyncSetUp()

self._path = os.path.join(self._test_parent_dir, "some_dir/v3io-py-test-emd")
self._path = url_join(self._test_parent_dir, "some_dir/v3io-py-test-emd")
await self._delete_dir(self._path)

async def test_kv_array(self):
Expand Down Expand Up @@ -374,7 +375,7 @@ async def test_kv_array(self):

async def test_kv_values(self):
def _get_int_array():
int_array = array.array("l")
int_array = array.array("q")
for value in range(10):
int_array.append(value)

Expand Down
14 changes: 14 additions & 0 deletions tests/test_client_errors.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# Copyright 2023 Iguazio
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import itertools

import pytest
Expand Down
32 changes: 32 additions & 0 deletions tests/test_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright 2023 Iguazio
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import unittest

from v3io.common.helpers import url_join
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this file should be called test_helpers.py?



class Test(unittest.TestCase):
def test_url_join(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably better make this a parameterized test, so that we can see which test cases pass and which fail.

self.assertEqual(url_join("a", "b"), "a/b") # add just exactly one "/" between parts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For whaterver reason, assertEqual treats the left side as the expected and right side as the actual. Opposite of a simple assert.

self.assertEqual(url_join("/", "a", "b"), "/a/b")
self.assertEqual(url_join("/", "a", "/b"), "/a/b")
self.assertEqual(url_join("/", "/a", "b"), "/a/b")
self.assertEqual(url_join("/", "/a", "/b"), "/a/b")
self.assertEqual(url_join("/", "/a/", "b"), "/a/b")
self.assertEqual(url_join("/", "/a/", "/b"), "/a/b")
self.assertEqual(url_join("a", "b"), "a/b") # keep suffix "/" exist/not-exist invariant
self.assertEqual(url_join("a", "b/"), "a/b/")
self.assertEqual(url_join("a", "b//"), "a/b//")
self.assertEqual(url_join("a", "b//", "/"), "a/b//") # suffix "/" count (if > 0) may change (but we don"t care)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.assertEqual(url_join("a", "b//", "/"), "a/b//") # suffix "/" count (if > 0) may change (but we don"t care)
self.assertEqual(url_join("a", "b//", "/"), "a/b//") # suffix "/" count (if > 0) may change (but we don't care)

Also, I wonder if we don't want to remove redundant slashes in any case.

7 changes: 3 additions & 4 deletions v3io/aio/dataplane/kv.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
import os

import v3io.aio.dataplane.kv_cursor
import v3io.dataplane.model
import v3io.dataplane.output
import v3io.dataplane.request
from v3io.common.helpers import url_join


class Model(v3io.dataplane.model.Model):
Expand Down Expand Up @@ -303,7 +302,7 @@ async def delete(self, container, table_path, key, access_key=None, raise_for_st
A `Response` object.
"""
return self._client.delete_object(
container, os.path.join(table_path, key), access_key, raise_for_status, transport_actions
container, url_join(table_path, key), access_key, raise_for_status, transport_actions
)

async def create_schema(self, container, table_path, access_key=None, raise_for_status=None, key=None, fields=None):
Expand Down Expand Up @@ -343,7 +342,7 @@ async def create_schema(self, container, table_path, access_key=None, raise_for_
A `Response` object
"""
put_object_args = locals()
put_object_args["path"] = os.path.join(put_object_args["table_path"], ".#schema")
put_object_args["path"] = url_join(put_object_args["table_path"], ".#schema")
put_object_args["offset"] = 0
put_object_args["append"] = None
put_object_args["body"] = self._client._get_schema_contents(key, fields)
Expand Down
7 changes: 3 additions & 4 deletions v3io/aio/dataplane/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
import os

import v3io.dataplane.kv_cursor
import v3io.dataplane.model
import v3io.dataplane.output
import v3io.dataplane.request
from v3io.common.helpers import url_join


class Model(v3io.dataplane.model.Model):
Expand Down Expand Up @@ -204,7 +203,7 @@ async def seek(
----------
A `Response` object, whose `output` is `SeekShardOutput`.
"""
stream_path = self._ensure_path_ends_with_slash(os.path.join(stream_path, str(shard_id)))
stream_path = self._ensure_path_ends_with_slash(url_join(stream_path, str(shard_id)))

return await self._transport.request(
container,
Expand Down Expand Up @@ -307,7 +306,7 @@ async def get_records(
----------
A `Response` object, whose `output` is `GetRecordsOutput`.
"""
stream_path = self._ensure_path_ends_with_slash(os.path.join(stream_path, str(shard_id)))
stream_path = self._ensure_path_ends_with_slash(url_join(stream_path, str(shard_id)))

return await self._transport.request(
container,
Expand Down
22 changes: 14 additions & 8 deletions v3io/common/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,22 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
def url_join(base, *parts):
result = base

if result[0] != "/":
result = "/" + base

for part in parts:
if part[0] != "/":
def url_join(*parts):
Copy link
Member

@gtopper gtopper Apr 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might be clearer:

def url_join(*parts):
    slash_suffix = False
    result = ""
    for part_index, part in enumerate(parts):
        if part == "":
            continue

        if slash_suffix:
            # if slash suffix existed in prev trim slash prefix from this part
            result += part.lstrip("/")
        else:
            # add slash prefix before part if:
            # 1. slash suffix did not exist in prev part and
            # 2. slash prefix does not exist in this part and
            # 3. part is not the first
            if part[0] != "/" and part_index != 0:
                result += "/"
            result += part

        slash_suffix = part[-1] == "/"
    return result

result = ""
slash_suffix = False
for part_index, part in enumerate(parts):
if part == "":
continue
# add slash prefix before part if:
# 1. slash suffix did not exit in prev part
# 2. slash prefix does not exit in this part
# 3. part is not the first
Comment on lines +23 to +26
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exit -> exist

Need to make it clear that all 3 points need to be true (could be read as just one of them).

if not slash_suffix and part[0] != "/" and part_index != 0:
result += "/" + part
else:
result += part

# if slash suffix existed in prev trim slash prefix from this part
result += part if not slash_suffix else part.lstrip("/")
slash_suffix = True if part[-1] == "/" else False
return result
Loading