SlideShare a Scribd company logo
(Ab)Using the
MetaCPAN API for
 Fun and Profit
  Olaf Alders (OALDERS)
     @wundercounter
Architecture


• Built on ElasticSearch
• Uses Catalyst as a thin wrapper
• You don’t need to know this
Real life examples
iCPAN - iPhone
iCPAN - iPad
Android
(Ab)Using the MetaCPAN API for Fun and Profit
(Ab)Using the MetaCPAN API for Fun and Profit
(Ab)Using the MetaCPAN API for Fun and Profit
(Ab)Using the MetaCPAN API for Fun and Profit
(Ab)Using the MetaCPAN API for Fun and Profit
(Ab)Using the MetaCPAN API for Fun and Profit
(Ab)Using the MetaCPAN API for Fun and Profit
(Ab)Using the MetaCPAN API for Fun and Profit
(Ab)Using the MetaCPAN API for Fun and Profit
(Ab)Using the MetaCPAN API for Fun and Profit
What can we build?
What can we build?


• Do something with Github
What can we build?


• Do something with Github
• Get a list of all CPAN authors who
  have enabled the “hireable” flag in
  their Github profiles
Let’s Get Started
Let’s Get Started



• We want to fetch some data
Let’s Get Started



• We want to fetch some data
• We’ll use Sawyer’s MetaCPAN::API
#!/usr/bin/env perl

use strict;
use warnings;

use MetaCPAN::API;

my $mcpan = MetaCPAN::API->new();
my $author = $mcpan->author('MSTROUT');
{
    dir            =>   "id/M/MS/MSTROUT",
    email          =>   ["perl-stuff@trout.me.uk"],
    gravatar_url   =>   "https://secure.gravatar.com/avatar/...",
    name           =>   "Matt S Trout",
    pauseid        =>   "MSTROUT",
    website        =>   ["http://www.trout.me.uk/"],
}
(Ab)Using the MetaCPAN API for Fun and Profit
(Ab)Using the MetaCPAN API for Fun and Profit
MetaCPAN Explorer
my $author = $mcpan->author('MSTROUT');
my $result = $mcpan->post(
    'author',
    {   query => { match_all => {} },
        size => 1,
    },
);
{
    _shards => { failed => 0, successful => 5, total => 5 },
    hits => {
       hits => [
          {
             _id     => "KHAMPTON",
             _index => "cpan_v1",
             _score => 1,
             _source => {
                           city         => "Los Angeles",
                           country      => "US",
                           dir          => "id/K/KH/KHAMPTON",
                           email        => ["khampton@totalcinema.com", "kip.hampton@tamarou.com"],
                           gravatar_url => "http://www.gravatar.com/avatar/...",
                           name         => "Kip Hampton",
                           pauseid      => "KHAMPTON",
                           profile      => [
                                              { id => "ubu", name => "coderwall" },
                                              { id => "ubu", name => "github" },
                                              { id => "kiphampton", name => "twitter" },
                                           ],
                           region       => "CA",
                           updated      => "2011-07-22T20:42:06",
                           website      => ["http://totalcinema.com/"],
                        },
             _type   => "author",
          },
       ],
       max_score => 1,
       total => 9780,
    },
    timed_out => bless(do{(my $o = 0)}, "JSON::XS::Boolean"),
    took => 1,
}
my $result = $mcpan->post(
    'author',
    {   query => { match_all => {} },
        size => 1,
    },
);

# dump $result->{hits}->{hits}->[0]->{_source};
{
  city         =>   "Los Angeles",
  country      =>   "US",
  dir          =>   "id/K/KH/KHAMPTON",
  email        =>   ["khampton@totalcinema.com", "kip.hampton
@tamarou.com"],
  gravatar_url => "http://www.gravatar.com/avatar/...",
  name         => "Kip Hampton",
  pauseid      => "KHAMPTON",
  profile      => [
                     { id => "ubu", name => "coderwall" },
                     { id => "ubu", name => "github" },
                     { id => "kiphampton", name => "twitter" },
                  ],
    region     => "CA",
    updated    => "2011-07-22T20:42:06",
    website    => ["http://totalcinema.com/"],
}
my $result = $mcpan->post(
    'author',
    {   query => { match_all => {} },
        size   => 100,
    },
);
my $filter = {
        { term   => { 'author.profile.name' => 'stackoverflow', } },
};

my $result = $mcpan->post(
    'author',
    {   query => { match_all => {} },
        filter => $filter,
        size   => 100,
    },
);
use Pithub;
my $p = Pithub->new;

AUTHOR:
foreach my $author ( @{ $result->{hits}->{hits} } ) {

    foreach my $profile ( @{ $author->{_source}->{profile} } ) {

        if ( $profile->{name} eq 'github' ) {

            my $username = $profile->{id};
            $username =~ s{https?://github.com/(w*)/?}{$1}i;
            next AUTHOR if !$username;

            if ( $p->users->get( user => $username )->content->{hireable} ) {
                # do something...
            }
            next AUTHOR;
        }
    }
}
Getting fancy
my $filter = {
    and => [
        { term   => { 'author.profile.name' => 'github', } },
        { term   => { 'author.country'      => 'US', } }
    ]
};

my $result = $mcpan->post(
    'author',
    {   query => { match_all => {} },
        filter => $filter,
        size   => 100,
    },
);
my $filter = {
    and => [
        { term     => { 'author.profile.name' => 'github', } },
        { term     => { 'author.country'      => 'US', } },
        { exists   => { 'field'               => 'author.region' } },
    ]
};

my $result = $mcpan->post(
    'author',
    {   query => { match_all => {} },
        filter => $filter,
        size   => 100,
    },
);
my $filter = {
    and => [
        { term      =>   {   'author.profile.name'   =>   'github', } },
        { term      =>   {   'author.country'        =>   'US', } },
        { exists    =>   {   'field'                 =>   'author.region' } },
        { missing   =>   {   'field'                 =>   'author.location' } },
    ]
};

my $result = $mcpan->post(
    'author',
    {   query => { match_all => {} },
        filter => $filter,
        size   => 100,
    },
);

# “missing” isn’t really helpful in this search
# just an example of how you might use it
my $filter = {
    or => [
        { term      =>   {   'author.profile.name'   =>   'github', } },
        { term      =>   {   'author.country'        =>   'US', } },
        { exists    =>   {   'field'                 =>   'author.region' } },
        { missing   =>   {   'field'                 =>   'author.location' } },
    ]
};

my $result = $mcpan->post(
    'author',
    {   query => { match_all => {} },
        filter => $filter,
        size   => 100,
    },
);
my $filter = {
    or => [
        { term      =>   {   'author.profile.name'   =>   'github', } },
        { term      =>   {   'author.country'        =>   'US', } },
        { exists    =>   {   'field'                 =>   'author.region' } },
        { missing   =>   {   'field'                 =>   'author.location' } },
    ]
};

my $result = $mcpan->post(
    'author',
    {   query => { match_all => {} },
        filter => $filter,
        size   => 100,
        fields => [ 'pauseid', 'country' ],
    },
);
my $filter = {
    or => [
        { term      =>   {   'author.profile.name'   =>   'github', } },
        { term      =>   {   'author.country'        =>   'US', } },
        { exists    =>   {   'field'                 =>   'author.region' } },
        { missing   =>   {   'field'                 =>   'author.location' } },
    ]
};

my $result = $mcpan->post(
    'author',
    {   query => { match_all => {} },
        filter => $filter,
        size   => 100,
        sort   => [ { 'author.pauseid' => 'ASC' } ],
    },
);
Getting Help



• #metacpan or irc.perl.org
• https://metacpan.org/about/resources
Resources


• https://github.com/CPAN-API/cpan-
  api/wiki/Beta-API-docs
• http://www.slideshare.net/
  clintongormley/terms-of-endearment-
  the-elasticsearch-query-dsl-explained
Bonus Slides
Base URL



• http://api.metacpan.org/v0
Convenience Endpoints
Convenience Endpoints

• /author/DOY
• /distribution/Moose
• /release/Moose
• /module/Moose
• /pod/Moose
(Ab)Using the MetaCPAN API for Fun and Profit
Exporting Pod


• /pod/Moose?content-type=text/html (default)
• /pod/Moose?content-type=text/plain
• /pod/Moose?content-type=text/x-pod
• /pod/Moose?content-type=text/x-markdown
The (real) Endpoints
The (real) Endpoints
• /author
The (real) Endpoints
• /author
• /distribution
The (real) Endpoints
• /author
• /distribution
• /favorite
The (real) Endpoints
• /author
• /distribution
• /favorite
• /rating
The (real) Endpoints
• /author
• /distribution
• /favorite
• /rating
• /release
The (real) Endpoints
• /author
• /distribution
• /favorite
• /rating
• /release
• /file
The (real) Endpoints
• /author
• /distribution
• /favorite
• /rating
• /release
• /file
(Ab)Using the MetaCPAN API for Fun and Profit
Using a cache

use HTTP::Tiny::Mech;
use MetaCPAN::API;
use WWW::Mechanize::Cached;

my $mcpan = MetaCPAN::API->new(
    ua => HTTP::Tiny::Mech->new(
        mechua => WWW::Mechanize::Cached->new()
    )
);
(Ab)Using the MetaCPAN API for Fun and Profit
Enable Compression


• use WWW::Mechanize::Gzip
• use WWW::Mechanize::Cached::Gzip
• Or set the appropriate request header
Use the scrolling API


• The scrolling API allows you to iterate
  over an arbitrary number of results
• Be aware that when you scroll, your
  docs will come back unsorted

More Related Content

PPT
PHP Tutorial (funtion)
ODP
Php 102: Out with the Bad, In with the Good
PDF
Doctrine MongoDB ODM (PDXPHP)
PDF
Leveraging the Power of Graph Databases in PHP
PPT
An Elephant of a Different Colour: Hack
PDF
How else can you write the code in PHP?
PDF
Leveraging the Power of Graph Databases in PHP
PHP Tutorial (funtion)
Php 102: Out with the Bad, In with the Good
Doctrine MongoDB ODM (PDXPHP)
Leveraging the Power of Graph Databases in PHP
An Elephant of a Different Colour: Hack
How else can you write the code in PHP?
Leveraging the Power of Graph Databases in PHP

What's hot (20)

KEY
Designing Opeation Oriented Web Applications / YAPC::Asia Tokyo 2011
PDF
Why Hacking WordPress Search Isn't Some Big Scary Thing
PDF
My First Ruby
TXT
DOC
PHP code examples
PDF
WordPress Cuztom Helper
KEY
DPC 2012 : PHP in the Dark Workshop
PDF
Perl6 in-production
KEY
Refactor like a boss
PDF
KEY
Introducing CakeEntity
PDF
Tips of CakePHP and MongoDB - Cakefest2011 ichikaway
KEY
Introducing CakeEntity
PDF
Crafting Custom Interfaces with Sub::Exporter
TXT
My shell
ZIP
全裸でワンライナー(仮)
PDF
DBIx::Class beginners
PDF
PHP 7 – What changed internally? (Forum PHP 2015)
PPT
Fantom and Tales
PDF
HappyKardashian.com for #FVCP
Designing Opeation Oriented Web Applications / YAPC::Asia Tokyo 2011
Why Hacking WordPress Search Isn't Some Big Scary Thing
My First Ruby
PHP code examples
WordPress Cuztom Helper
DPC 2012 : PHP in the Dark Workshop
Perl6 in-production
Refactor like a boss
Introducing CakeEntity
Tips of CakePHP and MongoDB - Cakefest2011 ichikaway
Introducing CakeEntity
Crafting Custom Interfaces with Sub::Exporter
My shell
全裸でワンライナー(仮)
DBIx::Class beginners
PHP 7 – What changed internally? (Forum PHP 2015)
Fantom and Tales
HappyKardashian.com for #FVCP
Ad

Viewers also liked (8)

ODP
Mangling
PDF
The MetaCPAN VM for Dummies Part One (Installation)
PDF
The MetaCPAN VM Part II (Using the VM)
PDF
No Hugging, No Learning
PDF
Ab(Using) the MetaCPAN API for Fun and Profit v2013
PDF
Git submodule
PDF
Modern Perl for Non-Perl Programmers
PDF
Medium Perl
Mangling
The MetaCPAN VM for Dummies Part One (Installation)
The MetaCPAN VM Part II (Using the VM)
No Hugging, No Learning
Ab(Using) the MetaCPAN API for Fun and Profit v2013
Git submodule
Modern Perl for Non-Perl Programmers
Medium Perl
Ad

Recently uploaded (20)

PPTX
Microbial diseases, their pathogenesis and prophylaxis
PDF
Business Ethics Teaching Materials for college
PPTX
The Healthy Child – Unit II | Child Health Nursing I | B.Sc Nursing 5th Semester
PPTX
Institutional Correction lecture only . . .
PPTX
human mycosis Human fungal infections are called human mycosis..pptx
PDF
VCE English Exam - Section C Student Revision Booklet
PDF
Anesthesia in Laparoscopic Surgery in India
PPTX
Final Presentation General Medicine 03-08-2024.pptx
PPTX
Cell Types and Its function , kingdom of life
PPTX
Pharma ospi slides which help in ospi learning
PDF
O5-L3 Freight Transport Ops (International) V1.pdf
PDF
Chapter 2 Heredity, Prenatal Development, and Birth.pdf
PPTX
Cell Structure & Organelles in detailed.
PDF
Physiotherapy_for_Respiratory_and_Cardiac_Problems WEBBER.pdf
PDF
Pre independence Education in Inndia.pdf
PDF
O7-L3 Supply Chain Operations - ICLT Program
PDF
Insiders guide to clinical Medicine.pdf
PPTX
Introduction_to_Human_Anatomy_and_Physiology_for_B.Pharm.pptx
PDF
Supply Chain Operations Speaking Notes -ICLT Program
PDF
Module 4: Burden of Disease Tutorial Slides S2 2025
Microbial diseases, their pathogenesis and prophylaxis
Business Ethics Teaching Materials for college
The Healthy Child – Unit II | Child Health Nursing I | B.Sc Nursing 5th Semester
Institutional Correction lecture only . . .
human mycosis Human fungal infections are called human mycosis..pptx
VCE English Exam - Section C Student Revision Booklet
Anesthesia in Laparoscopic Surgery in India
Final Presentation General Medicine 03-08-2024.pptx
Cell Types and Its function , kingdom of life
Pharma ospi slides which help in ospi learning
O5-L3 Freight Transport Ops (International) V1.pdf
Chapter 2 Heredity, Prenatal Development, and Birth.pdf
Cell Structure & Organelles in detailed.
Physiotherapy_for_Respiratory_and_Cardiac_Problems WEBBER.pdf
Pre independence Education in Inndia.pdf
O7-L3 Supply Chain Operations - ICLT Program
Insiders guide to clinical Medicine.pdf
Introduction_to_Human_Anatomy_and_Physiology_for_B.Pharm.pptx
Supply Chain Operations Speaking Notes -ICLT Program
Module 4: Burden of Disease Tutorial Slides S2 2025

(Ab)Using the MetaCPAN API for Fun and Profit

  • 1. (Ab)Using the MetaCPAN API for Fun and Profit Olaf Alders (OALDERS) @wundercounter
  • 2. Architecture • Built on ElasticSearch • Uses Catalyst as a thin wrapper • You don’t need to know this
  • 17. What can we build?
  • 18. What can we build? • Do something with Github
  • 19. What can we build? • Do something with Github • Get a list of all CPAN authors who have enabled the “hireable” flag in their Github profiles
  • 21. Let’s Get Started • We want to fetch some data
  • 22. Let’s Get Started • We want to fetch some data • We’ll use Sawyer’s MetaCPAN::API
  • 23. #!/usr/bin/env perl use strict; use warnings; use MetaCPAN::API; my $mcpan = MetaCPAN::API->new(); my $author = $mcpan->author('MSTROUT');
  • 24. { dir => "id/M/MS/MSTROUT", email => ["perl-stuff@trout.me.uk"], gravatar_url => "https://secure.gravatar.com/avatar/...", name => "Matt S Trout", pauseid => "MSTROUT", website => ["http://www.trout.me.uk/"], }
  • 28. my $author = $mcpan->author('MSTROUT');
  • 29. my $result = $mcpan->post( 'author', { query => { match_all => {} }, size => 1, }, );
  • 30. { _shards => { failed => 0, successful => 5, total => 5 }, hits => { hits => [ { _id => "KHAMPTON", _index => "cpan_v1", _score => 1, _source => { city => "Los Angeles", country => "US", dir => "id/K/KH/KHAMPTON", email => ["khampton@totalcinema.com", "kip.hampton@tamarou.com"], gravatar_url => "http://www.gravatar.com/avatar/...", name => "Kip Hampton", pauseid => "KHAMPTON", profile => [ { id => "ubu", name => "coderwall" }, { id => "ubu", name => "github" }, { id => "kiphampton", name => "twitter" }, ], region => "CA", updated => "2011-07-22T20:42:06", website => ["http://totalcinema.com/"], }, _type => "author", }, ], max_score => 1, total => 9780, }, timed_out => bless(do{(my $o = 0)}, "JSON::XS::Boolean"), took => 1, }
  • 31. my $result = $mcpan->post( 'author', { query => { match_all => {} }, size => 1, }, ); # dump $result->{hits}->{hits}->[0]->{_source};
  • 32. { city => "Los Angeles", country => "US", dir => "id/K/KH/KHAMPTON", email => ["khampton@totalcinema.com", "kip.hampton @tamarou.com"], gravatar_url => "http://www.gravatar.com/avatar/...", name => "Kip Hampton", pauseid => "KHAMPTON", profile => [ { id => "ubu", name => "coderwall" }, { id => "ubu", name => "github" }, { id => "kiphampton", name => "twitter" }, ], region => "CA", updated => "2011-07-22T20:42:06", website => ["http://totalcinema.com/"], }
  • 33. my $result = $mcpan->post( 'author', { query => { match_all => {} }, size => 100, }, );
  • 34. my $filter = { { term => { 'author.profile.name' => 'stackoverflow', } }, }; my $result = $mcpan->post( 'author', { query => { match_all => {} }, filter => $filter, size => 100, }, );
  • 35. use Pithub; my $p = Pithub->new; AUTHOR: foreach my $author ( @{ $result->{hits}->{hits} } ) { foreach my $profile ( @{ $author->{_source}->{profile} } ) { if ( $profile->{name} eq 'github' ) { my $username = $profile->{id}; $username =~ s{https?://github.com/(w*)/?}{$1}i; next AUTHOR if !$username; if ( $p->users->get( user => $username )->content->{hireable} ) { # do something... } next AUTHOR; } } }
  • 37. my $filter = { and => [ { term => { 'author.profile.name' => 'github', } }, { term => { 'author.country' => 'US', } } ] }; my $result = $mcpan->post( 'author', { query => { match_all => {} }, filter => $filter, size => 100, }, );
  • 38. my $filter = { and => [ { term => { 'author.profile.name' => 'github', } }, { term => { 'author.country' => 'US', } }, { exists => { 'field' => 'author.region' } }, ] }; my $result = $mcpan->post( 'author', { query => { match_all => {} }, filter => $filter, size => 100, }, );
  • 39. my $filter = { and => [ { term => { 'author.profile.name' => 'github', } }, { term => { 'author.country' => 'US', } }, { exists => { 'field' => 'author.region' } }, { missing => { 'field' => 'author.location' } }, ] }; my $result = $mcpan->post( 'author', { query => { match_all => {} }, filter => $filter, size => 100, }, ); # “missing” isn’t really helpful in this search # just an example of how you might use it
  • 40. my $filter = { or => [ { term => { 'author.profile.name' => 'github', } }, { term => { 'author.country' => 'US', } }, { exists => { 'field' => 'author.region' } }, { missing => { 'field' => 'author.location' } }, ] }; my $result = $mcpan->post( 'author', { query => { match_all => {} }, filter => $filter, size => 100, }, );
  • 41. my $filter = { or => [ { term => { 'author.profile.name' => 'github', } }, { term => { 'author.country' => 'US', } }, { exists => { 'field' => 'author.region' } }, { missing => { 'field' => 'author.location' } }, ] }; my $result = $mcpan->post( 'author', { query => { match_all => {} }, filter => $filter, size => 100, fields => [ 'pauseid', 'country' ], }, );
  • 42. my $filter = { or => [ { term => { 'author.profile.name' => 'github', } }, { term => { 'author.country' => 'US', } }, { exists => { 'field' => 'author.region' } }, { missing => { 'field' => 'author.location' } }, ] }; my $result = $mcpan->post( 'author', { query => { match_all => {} }, filter => $filter, size => 100, sort => [ { 'author.pauseid' => 'ASC' } ], }, );
  • 43. Getting Help • #metacpan or irc.perl.org • https://metacpan.org/about/resources
  • 44. Resources • https://github.com/CPAN-API/cpan- api/wiki/Beta-API-docs • http://www.slideshare.net/ clintongormley/terms-of-endearment- the-elasticsearch-query-dsl-explained
  • 48. Convenience Endpoints • /author/DOY • /distribution/Moose • /release/Moose • /module/Moose • /pod/Moose
  • 50. Exporting Pod • /pod/Moose?content-type=text/html (default) • /pod/Moose?content-type=text/plain • /pod/Moose?content-type=text/x-pod • /pod/Moose?content-type=text/x-markdown
  • 53. The (real) Endpoints • /author • /distribution
  • 54. The (real) Endpoints • /author • /distribution • /favorite
  • 55. The (real) Endpoints • /author • /distribution • /favorite • /rating
  • 56. The (real) Endpoints • /author • /distribution • /favorite • /rating • /release
  • 57. The (real) Endpoints • /author • /distribution • /favorite • /rating • /release • /file
  • 58. The (real) Endpoints • /author • /distribution • /favorite • /rating • /release • /file
  • 60. Using a cache use HTTP::Tiny::Mech; use MetaCPAN::API; use WWW::Mechanize::Cached; my $mcpan = MetaCPAN::API->new( ua => HTTP::Tiny::Mech->new( mechua => WWW::Mechanize::Cached->new() ) );
  • 62. Enable Compression • use WWW::Mechanize::Gzip • use WWW::Mechanize::Cached::Gzip • Or set the appropriate request header
  • 63. Use the scrolling API • The scrolling API allows you to iterate over an arbitrary number of results • Be aware that when you scroll, your docs will come back unsorted

Editor's Notes

  • #2: show of hands: \n1) have used the metacpan search site \n2) use it as their default search site \n3) have worked with the API\n
  • #3: \n
  • #4: \n
  • #5: \n
  • #6: \n
  • #7: \n
  • #8: \n
  • #9: \n
  • #10: \n
  • #11: CPAN visualization tool\n
  • #12: \n
  • #13: \n
  • #14: \n
  • #15: Exports Pod into a format you can import right into your Kindle app.\n
  • #16: \n
  • #17: Drop-in replacement for Perldoc. Read documentation for modules which you haven’t even installed. Genius.\n
  • #18: \n
  • #19: \n
  • #20: \n
  • #21: \n
  • #22: \n
  • #23: You can see that for the email and website fields, we allow you to provide a list rather than a single value. Now, for our example we need an author’s Github profile. Matt Trout does not provide this, so he’s a bad test case for our script.\n
  • #24: Things like StackOverflow, Twitter and Github usernames are all provided by authors voluntarily after logging in to MetaCPAN. In order to see what the profiles look like in a data structure, we need to find an author who has filled these fields.\n
  • #25: You can see from Mo’s example here that he has filled out some of his profile information. He’s a good test case. Note the MetaCPAN explorer link on the bottom left corner. These links can also be found on the module and release pages.\n
  • #26: This is a great way to explore the various endpoints of the API and practice crafting queries by hand. However, today we’re just concerned with the /author endpoint.\n
  • #27: \n
  • #28: \n
  • #29: You can see here that since we’re no longer using a convenience endpoint, the output is a little busier. What we generally care about here is the list provided inside of hits->{hits}. In each list item, we care about _source and _source->{profile} in particular.\n
  • #30: \n
  • #31: \n
  • #32: \n
  • #33: \n
  • #34: \n
  • #35: \n
  • #36: \n
  • #37: \n
  • #38: \n
  • #39: \n
  • #40: \n
  • #41: \n
  • #42: \n
  • #43: \n
  • #44: \n
  • #45: \n
  • #46: \n
  • #47: \n
  • #48: \n
  • #49: \n
  • #50: \n
  • #51: \n
  • #52: \n
  • #53: \n
  • #54: \n
  • #55: \n
  • #56: \n
  • #57: \n
  • #58: \n
  • #59: \n
  • #60: \n