This repository has been archived by the owner on Aug 31, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 114
Create dataset #8
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
50abfb1
Add create, set_public, publish
timwis efc0f0c
Add support for columns to create
timwis 7219569
Use proper accessor, double quotes
timwis abb27ca
Add test for create() #3
timwis f09be89
Use http for mock protocol, add test for set_public(). #3
timwis 7ea0f16
Touch-ups to set_public test
timwis d128280
add test for publish()
timwis 30586a6
create() uses kwargs in payload implicitly. add test & doc for tags. …
timwis 7c74460
rename set_public to set_permission and simplify test
timwis 01f25fe
Tidy method descriptors
timwis b7ee184
comment fix
timwis f107706
Support /resource prefix in publish method
timwis 9e9a66a
Add usage to README files. Closes #4
timwis 3347faf
Tidy up test_soda.py
timwis b8a532b
Use https prefix in test_soda
timwis be0851d
remove whitespace, remove limitation in readme.md
timwis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -62,7 +62,7 @@ def __init__(self, domain, app_token, username=None, password=None, | |
session_adapter["adapter"]) | ||
self.uri_prefix = session_adapter["prefix"] | ||
else: | ||
self.uri_prefix = "https" | ||
self.uri_prefix = "https://" | ||
|
||
def authentication_validation(self, username, password, access_token): | ||
''' | ||
|
@@ -76,8 +76,50 @@ def authentication_validation(self, username, password, access_token): | |
" OAuth2.0. Please use only one authentication" | ||
" method.") | ||
|
||
def create(self, file_object): | ||
raise NotImplementedError() | ||
def create(self, name, **kwargs): | ||
''' | ||
Create a dataset, including the field types. Optionally, specify args such as: | ||
description : description of the dataset | ||
columns : list of columns (see docs/tests for list structure) | ||
category : must exist in /admin/metadata | ||
tags : array of tag strings | ||
row_identifier : field name of primary key | ||
''' | ||
public = kwargs.pop("public", False) | ||
published = kwargs.pop("published", False) | ||
|
||
payload = {"name": name} | ||
|
||
if("row_identifier" in kwargs): | ||
payload["metadata"] = { | ||
"rowIdentifier": kwargs.pop("row_identifier", None) | ||
} | ||
|
||
payload.update(kwargs) | ||
payload = _clear_empty_values(payload) | ||
|
||
return self._perform_update("post", "/api/views.json", payload) | ||
|
||
def set_permission(self, resource, permission="private"): | ||
''' | ||
Set a dataset's permissions to private or public | ||
Options are private, public | ||
''' | ||
params = { | ||
"method": "setPermission", | ||
"value": "public.read" if permission == "public" else permission | ||
} | ||
resource = resource.rsplit("/", 1)[-1] # just get the dataset id | ||
|
||
return self._perform_request("put", "/api/views/" + resource, params=params) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea, you're right. Doing stuff like this is silly. I like the idea of only requiring the user to pass the 4x4 code + content-type extension and then building the URLs in the calls depending on which version of the API we are hitting. That's kind of asking a lot from you though, so I'll merge this first and do the cleanup in a separate commit. |
||
|
||
def publish(self, resource): | ||
''' | ||
The create() method creates a dataset in a "working copy" state. | ||
This method publishes it. | ||
''' | ||
resource = resource.rsplit("/", 1)[-1].split(".")[0] # just get the dataset id | ||
return self._perform_request("post", "/api/views/" + resource + "/publication.json") | ||
|
||
def get(self, resource, **kwargs): | ||
''' | ||
|
@@ -145,7 +187,7 @@ def replace(self, resource, payload): | |
return self._perform_update("put", resource, payload) | ||
|
||
def _perform_update(self, method, resource, payload): | ||
if isinstance(payload, list): | ||
if isinstance(payload, list) or isinstance(payload, dict): | ||
response = self._perform_request(method, resource, | ||
data=json.dumps(payload)) | ||
elif isinstance(payload, file): | ||
|
@@ -184,7 +226,7 @@ def _perform_request(self, request_type, resource, **kwargs): | |
raise Exception("Unknown request type. Supported request types are" | ||
": {0}".format(", ".join(request_type_methods))) | ||
|
||
uri = "{0}://{1}{2}".format(self.uri_prefix, self.domain, resource) | ||
uri = "{0}{1}{2}".format(self.uri_prefix, self.domain, resource) | ||
|
||
# set a timeout, just to be safe | ||
kwargs["timeout"] = 10 | ||
|
@@ -195,8 +237,9 @@ def _perform_request(self, request_type, resource, **kwargs): | |
if response.status_code not in (200, 202): | ||
_raise_for_status(response) | ||
|
||
# deletes have no content body, simply return the whole response | ||
if request_type == "delete": | ||
# when responses have no content body (ie. delete, set_permission), | ||
# simply return the whole response | ||
if not response.text: | ||
return response | ||
|
||
# for other request types, return most useful data | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
{ | ||
"id": "2frc-hyvj", | ||
"name": "Foo Bar", | ||
"averageRating": 0, | ||
"createdAt": 1448018696, | ||
"description": "test dataset", | ||
"downloadCount": 0, | ||
"newBackend": false, | ||
"numberOfComments": 0, | ||
"oid": 14929734, | ||
"publicationAppendEnabled": false, | ||
"publicationGroup": 5638965, | ||
"publicationStage": "unpublished", | ||
"rowIdentifierColumnId": 230641051, | ||
"rowsUpdatedAt": 1448018697, | ||
"rowsUpdatedBy": "gxfh-uqsf", | ||
"tableId": 5638965, | ||
"totalTimesRated": 0, | ||
"viewCount": 0, | ||
"viewLastModified": 1448018697, | ||
"viewType": "tabular", | ||
"columns": [ | ||
{ | ||
"id": 230641050, | ||
"name": "Foo", | ||
"dataTypeName": "text", | ||
"fieldName": "foo", | ||
"position": 1, | ||
"renderTypeName": "text", | ||
"tableColumnId": 32762225, | ||
"format": {} | ||
}, | ||
{ | ||
"id": 230641051, | ||
"name": "Bar", | ||
"dataTypeName": "number", | ||
"fieldName": "bar", | ||
"position": 2, | ||
"renderTypeName": "number", | ||
"tableColumnId": 32762226, | ||
"format": {} | ||
} | ||
], | ||
"metadata": { | ||
"rowIdentifier": 230641051 | ||
}, | ||
"owner": {}, | ||
"query": {}, | ||
"rights": [ | ||
"read", | ||
"write", | ||
"add", | ||
"delete", | ||
"grant", | ||
"add_column", | ||
"remove_column", | ||
"update_column", | ||
"update_view", | ||
"delete_view" | ||
], | ||
"tableAuthor": {}, | ||
"flags": [ | ||
"default" | ||
] | ||
} |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,7 @@ | |
import json | ||
|
||
|
||
PREFIX = "mock" | ||
PREFIX = "https://" | ||
DOMAIN = "fakedomain.com" | ||
PATH = "/songs.json" | ||
APPTOKEN = "FakeAppToken" | ||
|
@@ -113,7 +113,7 @@ def test_delete(): | |
client = Socrata(DOMAIN, APPTOKEN, username=USERNAME, password=PASSWORD, | ||
session_adapter=mock_adapter) | ||
|
||
uri = "{0}://{1}{2}".format(PREFIX, DOMAIN, PATH) | ||
uri = "{0}{1}{2}".format(PREFIX, DOMAIN, PATH) | ||
adapter.register_uri("DELETE", uri, status_code=200) | ||
response = client.delete(PATH) | ||
assert response.status_code == 200 | ||
|
@@ -124,14 +124,91 @@ def test_delete(): | |
assert isinstance(e, requests_mock.exceptions.NoMockAddress) | ||
finally: | ||
client.close() | ||
|
||
def test_create(): | ||
mock_adapter = {} | ||
mock_adapter["prefix"] = PREFIX | ||
adapter = requests_mock.Adapter() | ||
mock_adapter["adapter"] = adapter | ||
client = Socrata(DOMAIN, APPTOKEN, username=USERNAME, password=PASSWORD, | ||
session_adapter=mock_adapter) | ||
|
||
response_data = "create_foobar.txt" | ||
resource = "/api/views.json" | ||
set_up_mock(adapter, "POST", response_data, 200, resource=resource) | ||
|
||
columns = [ | ||
{"fieldName": "foo", "name": "Foo", "dataTypeName": "text"}, | ||
{"fieldName": "bar", "name": "Bar", "dataTypeName": "number"} | ||
] | ||
tags = ["foo", "bar"] | ||
response = client.create("Foo Bar", description="test dataset", | ||
columns=columns, tags=tags, row_identifier="bar") | ||
|
||
request = adapter.request_history[0] | ||
request_payload = json.loads(request.text) # can't figure out how to use .json | ||
|
||
# Test request payload | ||
for dataset_key in ["name", "description", "columns", "tags"]: | ||
assert dataset_key in request_payload | ||
|
||
for column_key in ["fieldName", "name", "dataTypeName"]: | ||
assert column_key in request_payload["columns"][0] | ||
|
||
# Test response | ||
assert isinstance(response, dict) | ||
assert len(response.get("id")) == 9 | ||
client.close() | ||
|
||
def test_set_permission(): | ||
mock_adapter = {} | ||
mock_adapter["prefix"] = PREFIX | ||
adapter = requests_mock.Adapter() | ||
mock_adapter["adapter"] = adapter | ||
client = Socrata(DOMAIN, APPTOKEN, username=USERNAME, password=PASSWORD, | ||
session_adapter=mock_adapter) | ||
|
||
response_data = "empty.txt" | ||
resource = "/api/views" + PATH | ||
set_up_mock(adapter, "PUT", response_data, 200, resource=resource) | ||
|
||
# Test response | ||
response = client.set_permission(PATH, "public") | ||
assert response.status_code == 200 | ||
|
||
# Test request | ||
request = adapter.request_history[0] | ||
qs = request.url.split("?")[-1] | ||
assert qs == "method=setPermission&value=public.read" | ||
client.close() | ||
|
||
def test_publish(): | ||
mock_adapter = {} | ||
mock_adapter["prefix"] = PREFIX | ||
adapter = requests_mock.Adapter() | ||
mock_adapter["adapter"] = adapter | ||
client = Socrata(DOMAIN, APPTOKEN, username=USERNAME, password=PASSWORD, | ||
session_adapter=mock_adapter) | ||
|
||
response_data = "create_foobar.txt" | ||
resource = "/api/views/songs/publication.json" # publish() removes .json | ||
set_up_mock(adapter, "POST", response_data, 200, resource=resource) | ||
|
||
response = client.publish("/resource/songs.json") # hard-coded so request uri is matched | ||
assert isinstance(response, dict) | ||
assert len(response.get("id")) == 9 | ||
client.close() | ||
|
||
def set_up_mock(adapter, method, response, response_code, | ||
reason="OK", auth=None): | ||
reason="OK", auth=None, resource=PATH): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Necessary to accommodate the more complicated paths these new methods introduce |
||
path = os.path.join(TEST_DATA_PATH, response) | ||
with open(path, "rb") as f: | ||
body = json.load(f) | ||
uri = "{0}://{1}{2}".format(PREFIX, DOMAIN, PATH) | ||
try: | ||
body = json.load(f) | ||
except ValueError: | ||
body = None # for case of empty file (test_set_permission) | ||
|
||
uri = "{0}{1}{2}".format(PREFIX, DOMAIN, resource) | ||
headers = { | ||
"content-type": "application/json; charset=utf-8" | ||
} | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
session.mount()
works when passedmock
but not whenhttps
- for https to work, it needs to be passedhttps://
. I'm not sure why this is, but the documentation suggests this approach fits fine. It required a few changes elsewhere, removing://
from a few string constructors. The alternative to doing that is to inject a://
into thesession.mount()
call, but I chose not to do that in case, in theory, you wanted to use a longer prefix likehttp://google.com
(which would work with this approach)