Wednesday, January 25, 2017

How to test a REST api from command line with curl

From http://www.codingpedia.org/ama/how-to-test-a-rest-api-from-command-line-with-curl/

How to test a REST api from command line with curl

If you want to quickly test your REST api from the command line, you can use curl. In this post I will present how to execute GET, POST, PUT, HEAD, DELETE HTTP Requests against a REST API. For the purpose of this blog post I will be using the REST api developed in my post Tutorial – REST API design and implementation in Java with Jersey and Spring

1. Introduction

If in the first part of the blog post I will do a brief introduction to curl and what it can do (HTTP requests with options), in the second part I will “translate” the SOAPui test suite developed for the REST API tutorial to curl requests.

1.1. What is curl?

 Curl is a command line tool and library for transferring data with URL syntax, supporting DICT, FILE, FTP, FTPS, Gopher, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, POP3, POP3S, RTMP, RTSP, SCP, SFTP, SMTP, SMTPS, Telnet and TFTP. curl supports SSL certificates, HTTP POST, HTTP PUT, FTP uploading, HTTP form based upload, proxies, HTTP/2, cookies, user+password authentication (Basic, Digest, NTLM, Negotiate, Kerberos…), file transfer resume, proxy tunneling and more.[1]
As mentioned, I will be using curl to simulate HEAD, GET, POST, PUT and DELETE request calls to the REST API.

1.2. HEAD requests

If you want to check if a resource is serviceable, what kind of headers it provides and other useful meta-information written in response headers, without having to transport the entire content, you can make a HEAD request.  Let’s say I want to see what I would GET when requesting a Podcast resource. I would issue the following HEAD request with curl:
Request
curl -I http://localhost:8888/demo-rest-jersey-spring/podcasts/1
OR
curl -i -X HEAD http://localhost:8888/demo-rest-jersey-spring/podcasts/1
Curl options 
  • -i, --include – include protocol headers in the output (H/F)
  • -X, --request – specify request  COMMAND (GET, PUT, DELETE…)  to use
Response
% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0   631    0     0    0     0      0      0 --:--:--  0:00:05 --:--:--     0
HTTP/1.1 200 OK
Date: Tue, 25 Nov 2014 12:54:56 GMT
Server: Jetty(9.0.7.v20131107)
Access-Control-Allow-Headers: X-extra-header
Access-Control-Allow-Headers: X-Requested-With, Content-Type, X-Codingpedia
Allow: OPTIONS
Content-Type: application/xml
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, DELETE, PUT
Vary: Accept-Encoding
Content-Length: 631
Note the following headers
  • Access-Control-Allow-Headers: Content-Type
  • Access-Control-Allow-Methods: GET, POST, DELETE, PUT and
  • Access-Control-Allow-Origin: *
in the response.
They’ve been added to support Cross-Origing Resource Sharing (CORS). You can find more about that in my post How to add CORS support on the server side in Java with Jersey.
What I find a little bit intriguing is the response header Content-Type: application/xml, because I would have expected it to be application/json, since in the resource method defined with Jersey this should have taken precedence:
@GET
@Path("{id}")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response getPodcastById(@PathParam("id") Long id, @QueryParam("detailed") boolean detailed)
        throws IOException, AppException {
    Podcast podcastById = podcastService.getPodcastById(id);
    return Response.status(200)
            .entity(podcastById, detailed ? new Annotation[]{PodcastDetailedView.Factory.get()} : new Annotation[0])
            .header("Access-Control-Allow-Headers", "X-extra-header")
            .allow("OPTIONS").build();
}

1.3. GET request

Executing curl with no parameters on a URL (resource) will execute a GET.
Request
curl http://localhost:8888/demo-rest-jersey-spring/podcasts/1
Response
<?xml version="1.0" encoding="UTF-8"?>
<podcast>
   <id>1</id>
   <title>- The Naked Scientists Podcast - Stripping Down Science</title>
   <linkOnPodcastpedia>http://www.podcastpedia.org/podcasts/792/-The-Naked-Scientists-Podcast-Stripping-Down-Science</linkOnPodcastpedia>
   <feed>feed_placeholder</feed>
   <description>The Naked Scientists flagship science show brings you a lighthearted look at the latest scientific breakthroughs, interviews with the world top scientists, answers to your science questions and science experiments to try at home.</description>
   <insertionDate>2014-10-29T10:46:02.00+0100</insertionDate>
</podcast>
Note that as expected from the HEAD request we get an xml document. Anyway we can force a JSON response by adding a header line to our curl request, setting the Accept HTTP header to application/json:
curl --header "Accept:application/json" http://localhost:8888/demo-rest-jersey-spring/podcasts/1
Curl options 
  • -H, --header – customer header to pass to the server
curl -H "Accept:application/json" http://localhost:8888/demo-rest-jersey-spring/podcasts/1
Response
{
  "id": 1,
  "title": "- The Naked Scientists Podcast - Stripping Down Science",
  "linkOnPodcastpedia": "http://www.podcastpedia.org/podcasts/792/-The-Naked-Scientists-Podcast-Stripping-Down-Science",
  "feed": "feed_placeholder",
  "description": "The Naked Scientists flagship science show brings you a lighthearted look at the latest scientific breakthroughs, interviews with the world top scientists, answers to your science questions and science experiments to try at home.",
  "insertionDate": "2014-10-29T10:46:02.00+0100"
}
If you want to have it displayed prettier, you can use the following command, provided you have Python installed on your machine.
Request
curl -H "Accept:application/json" http://localhost:8888/demo-rest-jersey-spring/podcasts/1 | python -m json.tool
Response
% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   758  100   758    0     0   6954      0 --:--:-- --:--:-- --:--:--  6954
[
    {
        "description": "The Naked Scientists flagship science show brings you a lighthearted look at the latest scientific breakthroughs, interviews with the world top scientists, answers to your science questions and science experiments to try at home.",
        "feed": "feed_placeholder",
        "id": 1,
        "insertionDate": "2014-10-29T10:46:02.00+0100",
        "linkOnPodcastpedia": "http://www.podcastpedia.org/podcasts/792/-The-Naked-Scientists-Podcast-Stripping-Down-Science",
        "title": "- The Naked Scientists Podcast - Stripping Down Science"
    },
    {
        "description": "Quarks & Co: Das Wissenschaftsmagazin",
        "feed": "http://podcast.wdr.de/quarks.xml",
        "id": 2,
        "insert
        ionDate": "2014-10-29T10:46:13.00+0100",
        "linkOnPodcastpedia": "http://www.podcastpedia.org/quarks",
        "title": "Quarks & Co - zum Mitnehmen"
    }
]

1.4. Curl request with multiple headers

As you’ve found out in my latest post, How to compress responses in Java REST API with GZip and Jersey, all the responses provided by the REST api are being compressed with GZip. This happens only if the client “suggests” that it accepts such encoding, by setting the following header Accept-encoding:gzip.
Request
curl -v -H "Accept:application/json" -H "Accept-encoding:gzip" http://localhost:8888/demo-rest-jersey-spring/podcasts/
Curl options 
  • -v, --verbose – make the operation more talkative
To achieve that you need to simply add another -H option with the corresponding value. Of course in this case you would get some unreadable characters in the content, if you do not redirect the response to a file:
* Adding handle: conn: 0x28ddd80
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0x28ddd80) send_pipe: 1, recv_pipe: 0
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0* About to connect() to proxy vldn680 port 19001 (#0)
*   Trying 10.32.142.80...
* Connected to vldn680 (10.32.142.80) port 19001 (#0)
> GET http://localhost:8888/demo-rest-jersey-spring/podcasts/ HTTP/1.1
> User-Agent: curl/7.30.0
> Host: localhost:8888
> Proxy-Connection: Keep-Alive
> Accept:application/json
> Accept-encoding:gzip
>
< HTTP/1.1 200 OK
< Date: Tue, 25 Nov 2014 16:17:02 GMT
* Server Jetty(9.0.7.v20131107) is not blacklisted
< Server: Jetty(9.0.7.v20131107)
< Content-Type: application/json
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET, POST, DELETE, PUT
< Access-Control-Allow-Headers: X-Requested-With, Content-Type, X-Codingpedia
< Vary: Accept-Encoding
< Content-Encoding: gzip
< Content-Length: 413
< Via: 1.1 vldn680:8888
<
{ [data not shown]
100   413  100   413    0     0   2647      0 --:--:-- --:--:-- --:--:--  2647▒QKo▒0▒+▒▒g▒▒R▒+{▒V▒Pe▒▒؊c▒▒      n▒▒▒▒fæHH▒"▒▒g▒/?2▒eM▒gl▒a▒d
▒{=`7▒EÏ–▒▒c▒ZM

n8▒i▒▒▒}H▒▒i1▒3g▒▒▒▒▒   ;▒E▒0O▒n▒R*▒g/E▒▒n=▒▒▒▒)▒U▒▒▒lÕª▒Φ▒h▒6▒▒▒_>w▒▒-▒▒:▒▒▒!▒Bb▒Z▒▒tO▒N@'= |▒▒C▒f▒▒loØ ▒,T▒▒A▒4▒▒:▒l+<▒▒▒▒P▒3▒▒A▒lR
▒u▒a▒͓9hO        #▒▒h▒i▒gq▒▒$▒▒|Ň        ▒▒▒08>#▒0b!▒▒'▒G▒^▒Iﺬ.TU▒▒▒z▒\▒i^]e▒▒▒▒2▒▒▒֯▒▒?▒:/▒m▒▒▒▒▒Y▒h▒▒▒_ä¶™V▒+R▒WT▒0▒?f{▒▒▒▒&▒l▒▒Sk▒iÔ½~▒▒▒▒▒▒n▒▒▒▒_V]į▒
* Connection #0 to host vldn680 left intact

2. SOAPui test suite translated to curl requests

As mentioned, in this second part I will map to curl requests the SOAPui test suite presented here.

2.1. Create podcast(s) resource

2.1.1. Delete all podcasts (preparation step)

Request
curl -i -X DELETE http://localhost:8888/demo-rest-jersey-spring/podcasts/
Response
HTTP/1.1 204 No Content
Date: Tue, 25 Nov 2014 14:10:17 GMT
Server: Jetty(9.0.7.v20131107)
Content-Type: text/html
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, DELETE, PUT
Access-Control-Allow-Headers: X-Requested-With, Content-Type, X-Codingpedia
Vary: Accept-Encoding
Via: 1.1 vldn680:8888
Content-Length: 0

2.1.2. POST new podcast without feed – 400 (BAD_REQUEST)

Request
curl -i -X POST -H "Content-Type:application/json" http://localhost:8888/demo-rest-jersey-spring/podcasts/ -d '{"title":"- The Naked Scientists Podcast - Stripping Down Science-new-title2","linkOnPodcastpedia":"http://www.podcastpedia.org/podcasts/792/-The-Naked-Scientists-Podcast-Stripping-Down-Science","description":"The Naked Scientists flagship science show brings you a lighthearted look at the latest scientific breakthroughs, interviews with the world top scientists, answers to your science questions and science experiments to try at home."}'
Response
HTTP/1.1 400 Bad Request
Date: Tue, 25 Nov 2014 15:12:11 GMT
Server: Jetty(9.0.7.v20131107)
Content-Type: application/json
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, DELETE, PUT
Access-Control-Allow-Headers: X-Requested-With, Content-Type, X-Codingpedia
Vary: Accept-Encoding
Content-Length: 271
Via: 1.1 vldn680:8888
Connection: close

{"status":400,"code":400,"message":"Provided data not sufficient for insertion","link":"http://www.codingpedia.org/ama/tutorial-rest-api-design-and-implementation-in-java-with-jersey-and-spring/","developerMessage":"Please verify that the feed is properly generated/set"}

2.1.3. POST new podcast correctly – 201 (CREATED)

Request
curl -i -X POST -H "Content-Type:application/json" http://localhost:8888/demo-rest-jersey-spring/podcasts/ -d '{"title":"- The Naked Scientists Podcast - Stripping Down Science","linkOnPodcastpedia":"http://www.podcastpedia.org/podcasts/792/-The-Naked-Scientists-Podcast-Stripping-Down-Science","feed":"feed_placeholder","description":"The Naked Scientists flagship science show brings you a lighthearted look at the latest scientific breakthroughs, interviews with the world top scientists, answers to your science questions and science experiments to try at home."}'
Response
HTTP/1.1 201 Created
Location: http://localhost:8888/demo-rest-jersey-spring/podcasts/2
Content-Type: text/html
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, DELETE, PUT
Access-Control-Allow-Headers: X-Requested-With, Content-Type, X-Codingpedia
Vary: Accept-Encoding
Content-Length: 60
Server: Jetty(9.0.7.v20131107)

A new podcast has been created AT THE LOCATION you specified

2.1.4. POST same podcast as before to receive – 409 (CONFLICT)

Request
curl -i -X POST -H "Content-Type:application/json" http://localhost:8888/demo-rest-jersey-spring/podcasts/ -d '{"title":"- The Naked Scientists Podcast - Stripping Down Science","linkOnPodcastpedia":"http://www.podcastpedia.org/podcasts/792/-The-Naked-Scientists-Podcast-Stripping-Down-Science","feed":"feed_placeholder","description":"The Naked Scientists flagship science show brings you a lighthearted look at the latest scientific breakthroughs, interviews with the world top scientists, answers to your science questions and science experiments to try at home."}'
Response
HTTP/1.1 409 Conflict
Date: Tue, 25 Nov 2014 15:58:39 GMT
Server: Jetty(9.0.7.v20131107)
Content-Type: application/json
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, DELETE, PUT
Access-Control-Allow-Headers: X-Requested-With, Content-Type, X-Codingpedia
Vary: Accept-Encoding
Content-Length: 300

{"status":409,"code":409,"message":"Podcast with feed already existing in the database with the id 1","link":"http://www.codingpedia.org/ama/tutorial-rest-api-design-and-implementation-in-java-with-jersey-and-spring/","developerMessage":"Please verify that the feed and title are properly generated"}

2.1.5. PUT new podcast at location – 201 (CREATED)

Request
curl -i -X PUT -H "Content-Type:application/json" http://localhost:8888/demo-rest-jersey-spring/podcasts/2 -d '{"id":2,"title":"Quarks & Co - zum Mitnehmen","linkOnPodcastpedia":"http://www.podcastpedia.org/quarks","feed":"http://podcast.wdr.de/quarks.xml","description":"Quarks & Co: Das Wissenschaftsmagazin"}'
Response
HTTP/1.1 201 Created
Location: http://localhost:8888/demo-rest-jersey-spring/podcasts/2
Content-Type: text/html
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, DELETE, PUT
Access-Control-Allow-Headers: X-Requested-With, Content-Type, X-Codingpedia
Vary: Accept-Encoding
Content-Length: 60
Server: Jetty(9.0.7.v20131107)

A new podcast has been created AT THE LOCATION you specified

2.2. Read podcast resource

2.2.1. GET new inserted podcast – 200 (OK)

Request
curl -v -H "Accept:application/json" http://localhost:8888/demo-rest-jersey-spring/podcasts/1 | python -m json.tool
Response
< HTTP/1.1 200 OK
< Access-Control-Allow-Headers: X-extra-header
< Access-Control-Allow-Headers: X-Requested-With, Content-Type, X-Codingpedia
< Allow: OPTIONS
< Content-Type: application/json
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET, POST, DELETE, PUT
< Vary: Accept-Encoding
< Content-Length: 192
* Server Jetty(9.0.7.v20131107) is not blacklisted
< Server: Jetty(9.0.7.v20131107)
<
{ [data not shown]
* STATE: PERFORM => DONE handle 0x600056180; line 1626 (connection #0)
100   192  100   192    0     0   2766      0 --:--:-- --:--:-- --:--:--  3254
* Connection #0 to host localhost left intact
* Expire cleared
{
    "feed": "http://podcast.wdr.de/quarks.xml",
    "id": 1,
    "insertionDate": "2014-06-05T22:35:34.00+0200",
    "linkOnPodcastpedia": "http://www.podcastpedia.org/quarks",
    "title": "Quarks & Co - zum Mitnehmen"
}

2.2.2. GET podcasts sorted by insertion date DESC – 200 (OK)

Request
curl -v -H "Accept:application/json" http://localhost:8888/demo-rest-jersey-spring/podcasts?orderByInsertionDate=DESC | python -m json.tool
Response
< HTTP/1.1 200 OK
< Content-Type: application/json
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET, POST, DELETE, PUT
< Access-Control-Allow-Headers: X-Requested-With, Content-Type, X-Codingpedia
< Vary: Accept-Encoding
< Content-Length: 419
* Server Jetty(9.0.7.v20131107) is not blacklisted
< Server: Jetty(9.0.7.v20131107)
<
  0   419    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0{ [data not shown]
* STATE: PERFORM => DONE handle 0x600056180; line 1626 (connection #0)
100   419  100   419    0     0   6044      0 --:--:-- --:--:-- --:--:--  6983
* Connection #0 to host localhost left intact
* Expire cleared
[
    {
        "feed": "http://podcast.wdr.de/quarks.xml",
        "id": 1,
        "insertionDate": "2014-06-05T22:35:34.00+0200",
        "linkOnPodcastpedia": "http://www.podcastpedia.org/quarks",
        "title": "Quarks & Co - zum Mitnehmen"
    },
    {
        "feed": "http://www.dayintechhistory.com/feed/podcast-2",
        "id": 2,
        "insertionDate": "2014-06-05T22:35:34.00+0200",
        "linkOnPodcastpedia": "http://www.podcastpedia.org/podcasts/766/Day-in-Tech-History",
        "title": "Day in Tech History"
    }
]

2.3. Update podcast resource

2.3.1. PUT not “complete” podcast for FULL update – 400 (BAD_REQUEST)

Request
curl -v -H "Content-Type:application/json" -X PUT http://localhost:8888/demo-rest-jersey-spring/podcasts/2 -d '{"id":2, "title":"Quarks & Co - zum Mitnehmen","linkOnPodcastpedia":"http://www.podcastpedia.org/quarks","feed":"http://podcast.wdr.de/quarks.xml"}'
Response
< HTTP/1.1 400 Bad Request
< Content-Type: application/json
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET, POST, DELETE, PUT
< Access-Control-Allow-Headers: X-Requested-With, Content-Type, X-Codingpedia
< Vary: Accept-Encoding
< Content-Length: 290
* Server Jetty(9.0.7.v20131107) is not blacklisted
< Server: Jetty(9.0.7.v20131107)
<
* STATE: PERFORM => DONE handle 0x600056180; line 1626 (connection #0)
* Connection #0 to host localhost left intact
* Expire cleared
{"status":400,"code":400,"message":"Please specify all properties for Full UPDATE","link":"http://www.codingpedia.org/ama/tutorial-rest-api-design-and-implementation-in-java-with-jersey-and-spring/","developerMessage":"required properties - id, title, feed, lnkOnPodcastpedia, description"}

2.3.2. PUT podcast for FULL update – 200 (OK)

Request
$ curl -v -H "Content-Type:application/json" -X PUT http://localhost:8888/demo-rest-jersey-spring/podcasts/2 -d '{"id":2, "title":"Quarks & Co - zum Mitnehmen","linkOnPodcastpedia":"http://www.podcastpedia.org/quarks","feed":"http://podcast.wdr.de/quarks.xml", "description":"Quarks & Co: Das Wissenschaftsmagazin"}'
Response
< HTTP/1.1 200 OK
< Location: http://localhost:8888/demo-rest-jersey-spring/podcasts/2
< Content-Type: text/html
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET, POST, DELETE, PUT
< Access-Control-Allow-Headers: X-Requested-With, Content-Type, X-Codingpedia
< Vary: Accept-Encoding
< Content-Length: 86
* Server Jetty(9.0.7.v20131107) is not blacklisted
< Server: Jetty(9.0.7.v20131107)
<
* STATE: PERFORM =&gt; DONE handle 0x600056180; line 1626 (connection #0)
* Connection #0 to host localhost left intact
* Expire cleared
The podcast you specified has been fully updated created AT THE LOCATION you specified

2.3.3. POST (partial update) for not existent podcast – 404 (NOT_FOUND)

Request
$ curl -v -H "Content-Type:application/json" -X POST http://localhost:8888/demo-rest-jersey-spring/podcasts/3 -d '{"title":"Quarks & Co - zum Mitnehmen - GREAT PODCAST"}' | python -m json.tool
Response
< HTTP/1.1 404 Not Found
< Content-Type: application/json
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET, POST, DELETE, PUT
< Access-Control-Allow-Headers: X-Requested-With, Content-Type, X-Codingpedia
< Vary: Accept-Encoding
< Content-Length: 306
* Server Jetty(9.0.7.v20131107) is not blacklisted
< Server: Jetty(9.0.7.v20131107)
<
{ [data not shown]
* STATE: PERFORM =&gt; DONE handle 0x600056180; line 1626 (connection #0)
100   361  100   306  100    55   9069   1630 --:--:-- --:--:-- --:--:-- 13304
* Connection #0 to host localhost left intact
* Expire cleared
{
    "code": 404,
    "developerMessage": "Please verify existence of data in the database for the id - 3",
    "link": "http://www.codingpedia.org/ama/tutorial-rest-api-design-and-implementation-in-java-with-jersey-and-spring/",
    "message": "The resource you are trying to update does not exist in the database",
    "status": 404
}

2.3.4. POST (partial update) podcast – 200 (OK)

Request
$ curl -v -H "Content-Type:application/json" -X POST http://localhost:8888/demo-rest-jersey-spring/podcasts/2 -d '{"title":"Quarks & Co - zum Mitnehmen - GREAT PODCAST"}'
Response
< HTTP/1.1 200 OK
< Content-Type: text/html
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET, POST, DELETE, PUT
< Access-Control-Allow-Headers: X-Requested-With, Content-Type, X-Codingpedia
< Vary: Accept-Encoding
< Content-Length: 55
* Server Jetty(9.0.7.v20131107) is not blacklisted
< Server: Jetty(9.0.7.v20131107)
<
* STATE: PERFORM =&gt; DONE handle 0x600056180; line 1626 (connection #0)
* Connection #0 to host localhost left intact
* Expire cleared
The podcast you specified has been successfully updated

2.4. DELETE resource

2.4.1. DELETE second inserted podcast – 204 (NO_CONTENT)

Request
$ curl -v -X DELETE http://localhost:8888/demo-rest-jersey-spring/podcasts/2
Response
< HTTP/1.1 204 No Content
< Content-Type: text/html
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET, POST, DELETE, PUT
< Access-Control-Allow-Headers: X-Requested-With, Content-Type, X-Codingpedia
< Vary: Accept-Encoding
* Server Jetty(9.0.7.v20131107) is not blacklisted
< Server: Jetty(9.0.7.v20131107)
<
* Excess found in a non pipelined read: excess = 42 url = /demo-rest-jersey-spring/podcasts/2 (zero-length body)
* STATE: PERFORM =&gt; DONE handle 0x600056180; line 1626 (connection #0)
* Connection #0 to host localhost left intact
* Expire cleared

2.4.2. GET deleted podcast – 404 (NOT_FOUND)

Request
curl -v http://localhost:8888/demo-rest-jersey-spring/podcasts/2 | python -m json.tool
Response
< HTTP/1.1 404 Not Found
< Content-Type: application/json
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET, POST, DELETE, PUT
< Access-Control-Allow-Headers: X-Requested-With, Content-Type, X-Codingpedia
< Vary: Accept-Encoding
< Content-Length: 306
* Server Jetty(9.0.7.v20131107) is not blacklisted
< Server: Jetty(9.0.7.v20131107)
<
{ [data not shown]
* STATE: PERFORM =&gt; DONE handle 0x600056180; line 1626 (connection #0)
100   306  100   306    0     0   8916      0 --:--:-- --:--:-- --:--:-- 13304
* Connection #0 to host localhost left intact
* Expire cleared
{
    "code": 404,
    "developerMessage": "Verify the existence of the podcast with the id 2 in the database",
    "link": "http://www.codingpedia.org/ama/tutorial-rest-api-design-and-implementation-in-java-with-jersey-and-spring/",
    "message": "The podcast you requested with id 2 was not found in the database",
    "status": 404
}

2.5. Bonus operations

2.5.1. Add podcast from application form urlencoded

Request
curl -v --data-urlencode "title=Day in Tech History" --data-urlencode "linkOnPodcastpedia=http://www.podcastpedia.org/podcasts/766/Day-in-Tech-History" --data-urlencode "feed=http://www.dayintechhistory.com/feed/podcast"
Response
< HTTP/1.1 201 Created
< Location: http://localhost:8888/demo-rest-jersey-spring/podcasts/null
< Content-Type: text/html
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET, POST, DELETE, PUT
< Access-Control-Allow-Headers: X-Requested-With, Content-Type, X-Codingpedia
< Vary: Accept-Encoding
< Content-Length: 81
* Server Jetty(9.0.7.v20131107) is not blacklisted
< Server: Jetty(9.0.7.v20131107)
<
* STATE: PERFORM =&gt; DONE handle 0x600056180; line 1626 (connection #0)
* Connection #0 to host localhost left intact
* Expire cleared
A new podcast/resource has been created at /demo-rest-jersey-spring/podcasts/null

Thursday, January 19, 2017

Great Python tech interview questions and explanations

The C3 class resolution algorithm for multiple class inheritance

If we are dealing with multiple inheritance, according to the newer C3 class resolution algorithm, the following applies:
Assuming that child class C inherits from two parent classes A and B, “class A should be checked before class B”.
If you want to learn more, please read the original blog post by Guido van Rossum.
class A(object):
    def foo(self):
        print("class A")

class B(object):
    def foo(self):
        print("class B")

class C(A, B):
    pass

C().foo()
class A
So what actually happened above was that class C looked in the scope of the parent class A for the method .foo() first (and found it)!
I received an email containing a suggestion which uses a more nested example to illustrate Guido van Rossum’s point a little bit better:
class A(object):
   def foo(self):
      print("class A")

class B(A):
   pass

class C(A):
   def foo(self):
      print("class C")

class D(B,C):
   pass

D().foo()
class C
Here, class D searches in B first, which in turn inherits from A (note that class C also inherits from A, but has its own .foo() method) so that we come up with the search order: D, B, C, A.

Assignment operators and lists - simple-add vs. add-AND operators

Python lists are mutable objects as we all know. So, if we are using the += operator on lists, we extend the list by directly modifying the object directly.
However, if we use the assignment via my_list = my_list + ..., we create a new list object, which can be demonstrated by the following code:
a_list = []
print('ID:', id(a_list))

a_list += [1]
print('ID (+=):', id(a_list))

a_list = a_list + [2]
print('ID (list = list + ...):', id(a_list))
ID: 4366496544
ID (+=): 4366496544
ID (list = list + ...): 4366495472
Just for reference, the .append() and .extends() methods are modifying the list object in place, just as expected.
a_list = []
print(a_list, '\nID (initial):',id(a_list), '\n')

a_list.append(1)
print(a_list, '\nID (append):',id(a_list), '\n')

a_list.extend([2])
print(a_list, '\nID (extend):',id(a_list))
[]
ID (initial): 140704077653128

[1]
ID (append): 140704077653128

[1, 2]
ID (extend): 140704077653128

Python reuses objects for small integers - use “==” for equality, “is” for identity

This oddity occurs, because Python keeps an array of small integer objects (i.e., integers between -5 and 256, see the doc).
a = 1
b = 1
print('a is b', bool(a is b))
True

c = 999
d = 999
print('c is d', bool(c is d))
a is b True
c is d False
(I received a comment that this is in fact a CPython artefact and must not necessarily be true in all implementations of Python!)
So the take home message is: always use “==” for equality, “is” for identity!
Here is a nice article explaining it by comparing “boxes” (C language) with “name tags” (Python).
This example demonstrates that this applies indeed for integers in the range in -5 to 256:
print('256 is 257-1', 256 is 257-1)
print('257 is 258-1', 257 is 258 - 1)
print('-5 is -6+1', -5 is -6+1)
print('-7 is -6-1', -7 is -6-1)
256 is 257-1 True
257 is 258-1 False
-5 is -6+1 True
-7 is -6-1 False

And to illustrate the test for equality (==) vs. identity (is):

a = 'hello world!'
b = 'hello world!'
print('a is b,', a is b)
print('a == b,', a == b)
a is b, False
a == b, True
We would think that identity would always imply equality, but this is not always true, as we can see in the next example:
a = float('nan')
print('a is a,', a is a)
print('a == a,', a == a)
a is a, True
a == a, False

Shallow vs. deep copies if list contains other structures and objects

Shallow copy:
If we use the assignment operator to assign one list to another list, we just create a new name reference to the original list. If we want to create a new list object, we have to make a copy of the original list. This can be done via a_list[:] or a_list.copy().
list1 = [1,2]
list2 = list1        # reference
list3 = list1[:]     # shallow copy
list4 = list1.copy() # shallow copy

print('IDs:\nlist1: {}\nlist2: {}\nlist3: {}\nlist4: {}\n'
      .format(id(list1), id(list2), id(list3), id(list4)))

list2[0] = 3
print('list1:', list1)

list3[0] = 4
list4[1] = 4
print('list1:', list1)
IDs:
list1: 4346366472
list2: 4346366472
list3: 4346366408
list4: 4346366536

list1: [3, 2]
list1: [3, 2]
Deep copy
As we have seen above, a shallow copy works fine if we want to create a new list with contents of the original list which we want to modify independently.
However, if we are dealing with compound objects (e.g., lists that contain other lists, read here for more information) it becomes a little trickier.
In the case of compound objects, a shallow copy would create a new compound object, but it would just insert the references to the contained objects into the new compound object. In contrast, a deep copy would go “deeper” and create also new objects
for the objects found in the original compound object. If you follow the code, the concept should become more clear:
from copy import deepcopy

list1 = [[1],[2]]
list2 = list1.copy()    # shallow copy
list3 = deepcopy(list1) # deep copy

print('IDs:\nlist1: {}\nlist2: {}\nlist3: {}\n'
      .format(id(list1), id(list2), id(list3)))

list2[0][0] = 3
print('list1:', list1)

list3[0][0] = 5
print('list1:', list1)
IDs:
list1: 4377956296
list2: 4377961752
list3: 4377954928

list1: [[3], [2]]
list1: [[3], [2]]

Don’t use mutable objects as default arguments for functions!

Don’t use mutable objects (e.g., dictionaries, lists, sets, etc.) as default arguments for functions! You might expect that a new list is created every time when we call the function without providing an argument for the default parameter, but this is not the case: Python will create the mutable object (default parameter) the first time the function is defined - not when it is called, see the following code:
def append_to_list(value, def_list=[]):
    def_list.append(value)
    return def_list

my_list = append_to_list(1)
print(my_list)

my_other_list = append_to_list(2)
print(my_other_list)
[1]
[1, 2]
Another good example showing that demonstrates that default arguments are created when the function is created (and not when it is called!):
import time
def report_arg(my_default=time.time()):
    print(my_default)

report_arg()

time.sleep(5)

report_arg()
1397764090.456688
1397764090.456688

Python’s LEGB scope resolution and the keywords global and nonlocal

There is nothing particularly surprising about Python’s LEGB scope resolution (Local -> Enclosed -> Global -> Built-in), but it is still useful to take a look at some examples!

global vs. local

According to the LEGB rule, Python will first look for a variable in the local scope. So if we set the variable x = 1 locally in the function’s scope, it won’t have an effect on the global x.
x = 0
def in_func():
    x = 1
    print('in_func:', x)

in_func()
print('global:', x)
in_func: 1
global: 0
If we want to modify the global x via a function, we can simply use the global keyword to import the variable into the function’s scope:
x = 0
def in_func():
    global x
    x = 1
    print('in_func:', x)

in_func()
print('global:', x)
in_func: 1
global: 1

local vs. enclosed

Now, let us take a look at local vs. enclosed. Here, we set the variable x = 1 in the outer function and set x = 1 in the enclosed function inner. Since inner looks in the local scope first, it won’t modify outer’s x.
def outer():
       x = 1
       print('outer before:', x)
       def inner():
           x = 2
           print("inner:", x)
       inner()
       print("outer after:", x)
outer()
outer before: 1
inner: 2
outer after: 1
Here is where the nonlocal keyword comes in handy - it allows us to modify the x variable in the enclosed scope:
def outer():
       x = 1
       print('outer before:', x)
       def inner():
           nonlocal x
           x = 2
           print("inner:", x)
       inner()
       print("outer after:", x)
outer()
outer before: 1
inner: 2
outer after: 2