diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..9a883d851 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,38 @@ +--- +# Use the latest 2.1 version of CircleCI pipeline process engine. See: +# https://circleci.com/docs/2.0/configuration-reference +version: 2.1 +orbs: + codecov: codecov/codecov@4.1.0 +# Orchestrate or schedule a set of jobs +workflows: + docker-compose: + jobs: + - build-and-test +jobs: + build-and-test: + machine: true + resource_class: large + steps: + - run: + name: docker compose version + command: docker compose version + - checkout + - run: + name: create coverage directory + command: | + mkdir cover_db + chmod o+w cover_db + - run: + name: docker compose build + command: | + docker compose --profile test build api-test + - run: + name: run tests with coverage + command: | + docker compose --profile test run --env HARNESS_PERL_SWITCHES=-MDevel::Cover -v ./cover_db:/app/cover_db/ api-test bash -c 'prove -lr -j4 t && cover -report codecovbash' + # We are relying on environment variables from the host to be available when + # we publish the report, so we publish from the host rather than trying + # to propagate env variables to the container. + - codecov/upload: + file: cover_db/codecov.json diff --git a/.dockerignore b/.dockerignore new file mode 120000 index 000000000..3e4e48b0b --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.gitignore \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..7ff72b0cc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +indent_style = space +indent_size = 4 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 + +# I'd like to enable this, but we should fix all the files first to avoid diff noise. +#trim_trailing_whitespace = true +insert_final_newline = true + +# yaml indents are weird +[*.{yml,yaml}] +indent_size = 2 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 000000000..0a9225563 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,143 @@ +# How to contribute + +We are always after more contributors and suggestions. + +### How can I help? + +The following issues are tagged as [Volunteer needed](https://github.com/CPAN-API/cpan-api/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3A%22Volunteer+needed%22+no%3Aassignee+) + +## Suggestions or issues with MetaCPAN... + +#### Does it relate to our API (backend)... ? + + 1. Please check the [previously reported API issues](https://github.com/CPAN-API/cpan-api/issues) + 2. Please check the [Wishlist](https://github.com/CPAN-API/cpan-api/wiki/Wishlist). If you can't find it already there: + * If it's a wishlist idea, please edit the [wiki](https://github.com/CPAN-API/cpan-api/wiki/Wishlist) (add a 'wishlist_MYIDEA' page if you need more space!) + * If it's an actual bug [create a new issue](https://github.com/CPAN-API/cpan-api/issues/new) + +#### If you are not sure, or it is related to https://metacpan.org/ front end: + + 1. Please check the [previously reported Web issues](https://github.com/CPAN-API/metacpan-web/issues) + 2. Please check the [Wishlist](https://github.com/CPAN-API/cpan-api/wiki/Wishlist). If you can't find it already there: + * If it's a wishlist idea, please edit the [wiki](https://github.com/CPAN-API/cpan-api/wiki/Wishlist) (add a 'wishlist_MYIDEA' page if you need more space!) + * If it's an actual bug [create a new issue](https://github.com/CPAN-API/metacpan-web/issues/new) + +## Contributing code + +Come talk to us on IRC (see below), or send a pull request and we'll respond +there. If you implement a new feature, please add a note about it to the +News.md file at the top level of metacpan-web so that it will appear in our +news feed. + +If you aren't using the VM, remember to enable the pre-commit hook before you start working. + + sh git/setup.sh + +These links will get you going quickly: + + * [Using our developer VM](https://github.com/CPAN-API/metacpan-developer) to get you going in minutes (depending on bandwidth) + * [Front end bug list](https://github.com/CPAN-API/metacpan-web/issues) + * [API (back end) bug list](https://github.com/CPAN-API/cpan-api/issues) + * [Wishlist](https://github.com/CPAN-API/cpan-api/wiki/Wishlist) - things that probably need doing + +# Git workflow + +We try to keep a clean git history, so if it all possible, please rebase to get +the latest changes from master _before_ submitting a pull request. You'll only +need to do the first command (git remote add) once in your local checkout. + + git remote add upstream https://github.com/CPAN-API/metacpan-web.git + git pull --rebase upstream master + +If you are comfortable rebasing, it is also helpful to squash or delete commits +which are no longer relevant to your branch before submitting your work. + + git rebase -i master + +If you are not comfortable with rebasing, but want to use it, check out the steps +from [here](https://help.github.com/articles/using-git-rebase/). + +# Coding conventions + +Please try to follow the conventions already been used in the code base. This +will generally be the right thing to do. Our standards are improving, so even +if you do follow what you see, we may ask you to make some changes, but that is +a good thing. We are trying to keep things tidy. + +If you are using the [developer VM](https://github.com/CPAN-API/metacpan-developer) you can run: + +```sh +/home/vagrant/carton/metacpan-web/bin/tidyall +``` + +## Perl Best Practices + +In general, the concepts discussed in "Perl Best Practices" are a good starting +point. Use autodie where possible and MetaCPAN::Web::Types when creating new +Moose attributes. Many of the other standards will be enforced by Perl::Critic. + +## Clear > Concise + +Take pains to use variable names which are easy to understand and to write +readable code. We value readable code over concise code. Use singular nouns +for class names. Use verbs for method names. + +## Try::Tiny > eval { ... } + +You will see many eval statements in the code. We would like to standardize on +Try::Tiny, so feel free to swap out any eval with a Try::Tiny and use Try::Tiny +in all new code. + +## Prefer single quotes + +Always use single quotes in cases where there is no variable interpolation. If +there is a single quote in the quoted item, use curly quotes. + +q{Isn't this a lovely day}; + +## Include a test (or more!) + +Any time when a pull request includes a test, it makes it easier for us to +review and accept, so please do test your changes whenever possible. If your +pull request includes visual changes, please include a before and after screen +shot, so that we can better understand the problem you're trying to solve. + +## Dependencies + +Introducing new dependencies is fine, if they solve a specific problem which +current dependencies cannot address. If we prefer a different module to be used, +we'll let you know. + +## It's OK to be controversial + +If a pull request contains any controversial changes, we'll likely wait for some +feedback from several developers before a merge. If you think your changes may +be controversial, feel free to discuss them in a Github issue before starting to +write any code. + +## Travis is your friend + +We use Travis to test all code changes. After submitting your pull request, +remember to check back to see whether Travis has come back with any test +failures. We do get some false negatives. If your pull request failed for +reasons unrelated to your changes, we may still be able to merge your work. + +# Additional Resources + + * [\#metacpan](http://widget01.mibbit.com/?autoConnect=true&server=irc.perl.org&channel=%23metacpan&nick=) IRC channel on irc.perl.org + +# Current Policies + +### What is indexed? + + * Perl distributions which contain Perl packages. + +### When are issues closed? + +We want to keep the issue list manageable, so we can focus on what actually +needs fixing. If you feel an issue needs opening again, please add a comment +explaining why it needs re-opening and we'll look at it again. + + * Issues will be closed and moved to [Wishlist](https://github.com/CPAN-API/cpan-api/wiki/Wishlist) if they are not actual bugs + * Issues we think we have addressed will be closed + * Issues we are not going to take any further action on without more information will be closed diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..326ba5a61 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,18 @@ +# Important, please read: + +MetaCPAN's core developers need to focus on fixing bugs and improving the +existing core system. + +For this reason, if you have a feature which you would like to see added (there +are loads we would love to have), please only open an issue _IF_ you are +prepared to do the work to implement it. To be clear, we'd love to have a +bunch of really cool, new, features, but it's more important for us to focus on +keeping MetaCPAN humming along. + +If you're not motivated or otherwise able to send a pull request for your cool, +new feature, please add it to our wishlist: +https://github.com/CPAN-API/cpan-api/wiki/Wishlist and someone may get to it +one day. Maybe that person will be you! + +For more details on issues and contributing please see CONTRIBUTING.md (linked +above). diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..675a53345 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +--- +version: 2 +updates: + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + # Check for updates to GitHub Actions every week + interval: 'weekly' diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml new file mode 100644 index 000000000..a9b56fc7e --- /dev/null +++ b/.github/workflows/automerge.yml @@ -0,0 +1,22 @@ +name: Enable Auto-Merge For bots +on: + pull_request_target: + types: [opened] + +jobs: + enable-auto-merge: + runs-on: ubuntu-latest + if: > + github.event.pull_request.user.login == 'metacpan-automation[bot]' + || github.event.pull_request.user.login == 'dependabot[bot]' + steps: + - name: Generate Auth Token + uses: actions/create-github-app-token@v2 + id: app-token + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + - uses: peter-evans/enable-pull-request-automerge@v3 + with: + token: ${{ steps.app-token.outputs.token }} + pull-request-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml new file mode 100644 index 000000000..00d325bee --- /dev/null +++ b/.github/workflows/build-container.yml @@ -0,0 +1,44 @@ +name: Build container +on: + push: + branches: + - master + - staging + - prod + pull_request: + types: [opened, synchronize, labeled] + branches: + - master + workflow_dispatch: +jobs: + docker-build: + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'build-container') + runs-on: ubuntu-22.04 + name: Docker Build and Push + steps: + - name: Generate Auth Token + uses: actions/create-github-app-token@v2 + id: app-token + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + owner: metacpan + - uses: actions/checkout@v5 + with: + token: ${{ steps.app-token.outputs.token }} + - uses: metacpan/metacpan-actions/docker-build-push@master + id: build-push + with: + docker_hub_username: ${{ secrets.DOCKER_HUB_USER }} + docker_hub_password: ${{ secrets.DOCKER_HUB_TOKEN }} + ghcr_username: ${{ github.repository_owner }} + ghcr_password: ${{ secrets.GITHUB_TOKEN }} + - name: Update deployed image + if: ${{ fromJSON(steps.build-push.outputs.tag-fq).latest }} + uses: metacpan/metacpan-actions/update-deployed-tag@master + with: + token: ${{ steps.app-token.outputs.token }} + app: api + environment: prod + base-tag: ${{ fromJSON(steps.build-push.outputs.tag-fq).latest }} + tag: ${{ fromJSON(steps.build-push.outputs.tag-fq).sha }} diff --git a/.github/workflows/code-formatting.yml b/.github/workflows/code-formatting.yml new file mode 100644 index 000000000..b833d372c --- /dev/null +++ b/.github/workflows/code-formatting.yml @@ -0,0 +1,49 @@ +--- +name: Code Formatting +on: + push: + branches: + - 'master' + merge_group: + pull_request: + branches: + - '*' + workflow_dispatch: + +jobs: + code-formatting: + runs-on: ubuntu-24.04 + name: Code Formatting + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - name: Fetch base ref + if: ${{ github.event.pull_request }} + run: git fetch origin ${{ github.base_ref }}:upstream + - name: Install Carton + uses: perl-actions/install-with-cpm@v1 + with: + install: Carton + - name: Install CPAN deps + uses: perl-actions/install-with-cpm@v1 + with: + cpanfile: 'cpanfile' + args: > + --resolver=snapshot + --with-develop + - name: Install precious + run: ./bin/install-precious /usr/local/bin + env: + GITHUB_TOKEN: ${{ github.token }} + - run: perltidy --version + - name: Select files + id: select-files + run: | + if [[ -n "${{ github.event.pull_request.number }}" ]]; then + echo 'precious-args=--git-diff-from upstream' >> "$GITHUB_OUTPUT" + else + echo 'precious-args=--all' >> "$GITHUB_OUTPUT" + fi + - name: Lint files + run: precious lint ${{ steps.select-files.outputs.precious-args }} diff --git a/.github/workflows/update-snapshot.yml b/.github/workflows/update-snapshot.yml new file mode 100644 index 000000000..816301081 --- /dev/null +++ b/.github/workflows/update-snapshot.yml @@ -0,0 +1,35 @@ +name: Update cpanfile.snapshot +on: + schedule: + - cron: "1 15 * * 0" + workflow_dispatch: +jobs: + update-dep: + runs-on: "ubuntu-22.04" + container: + image: perl:5.22-buster + steps: + - name: Generate Auth Token + uses: actions/create-github-app-token@v2 + id: app-token + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + - uses: haarg/setup-git-user@v1 + with: + app: ${{ steps.app-token.output.app-slug }} + - uses: actions/checkout@v5 + with: + token: ${{ steps.app-token.outputs.token }} + - name: Update cpanfile.snapshot + uses: metacpan/metacpan-actions/update-snapshot@master + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ steps.app-token.outputs.token }} + commit-message: Update cpanfile.snapshot + title: Update cpanfile.snapshot + sign-commits: true + body: | + [GitHub Action Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + branch: update-cpanfile-snapshot diff --git a/.gitignore b/.gitignore index b82fcb8ae..cd6e79b24 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,25 @@ -.DS_Store -*.sw* -*.kpf +/MYMETA.* +/Makefile +/Makefile.old +/blib +/cover_db/ +/local/ +/log4perl_local.conf +/metacpan_server_local.* +/metacpan_server_testing_local.* +/perltidy.LOG +/pm_to_blib +/var +/bin/omegasort +/bin/precious +/bin/ubi +/etc/metacpan_local.pl +/t/var/darkpan/ +/t/var/log/ +/t/var/tmp/ *.komodoproject +*.kpf *.sqlite* -/var/tmp/ -/var/log/metacpan.* -/t/var/tmp/ -/etc/metacpan_local.pl -metacpan_server_local.conf +*.sw* +.DS_Store +.tidyall.d diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index b09098ca4..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "inc/hidek/Plack-Middleware-Auth-OAuth"] - path = inc/hidek/Plack-Middleware-Auth-OAuth - url = https://github.com/hidek/Plack-Middleware-Auth-OAuth.git diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000..8c9325def --- /dev/null +++ b/.mailmap @@ -0,0 +1,53 @@ +# shared between metacpan-web and metacpan-api + +Amrita Mathew +Andreas Marienborg +Andreea Pirvulescu +Andreea Pirvulescu +Andrew Fresh +Andrew Fresh +Barry Walsh +Brad Lhotsky +Chris Nehren +Christopher White +Christopher White +Christopher White +Clinton Gormley +Ed J +Gabor Szabo +Grant McLean +Grant McLean +J. Bobby Lopez +Jess Robinson +Joel Berger +Johannes Plunien +Mario Zieschang +Mark Fowler +Matthew Horsfall (alh) +Michael Peters +Michael Peters +Michiel Beijen +Mickey Nasriachi +Mickey Nasriachi +Mickey Nasriachi +Moritz Onken +Nicolas R +Olaf Alders +Olaf Alders +Olaf Alders +Randy Stauner +Renee Baecker +Shawn M Moore +Shawn Sorichetti +Sunny Patel +Sunny Patel +Talina Shrotriya +Talina Shrotriya +Talina Shrotriya Talina06 <--global> +Thomas Sibley +Tim Bunce +Vyacheslav Matyukhin +Zachary Dykstra +lnation +oiami +oiami diff --git a/.perlcriticrc b/.perlcriticrc new file mode 100644 index 000000000..578566da8 --- /dev/null +++ b/.perlcriticrc @@ -0,0 +1,40 @@ +# please alpha sort config items as you add them + +severity = 5 +verbose = 11 +theme = core + +[-ControlStructures::ProhibitPostfixControls] +[-Documentation::RequirePodSections] +[-InputOutput::ProhibitInteractiveTest] +[-Modules::RequireVersionVar] +[-RegularExpressions::RequireDotMatchAnything] +[-RegularExpressions::RequireExtendedFormatting] +[-RegularExpressions::RequireLineBoundaryMatching] +[-Subroutines::ProhibitExplicitReturnUndef] +[-TestingAndDebugging::ProhibitNoStrict] +[-ValuesAndExpressions::ProhibitNoisyQuotes] +[-Variables::ProhibitPunctuationVars] + +# doesn't understand signatures +[-Subroutines::ProhibitSubroutinePrototypes] + +[CodeLayout::RequireTrailingCommas] +severity = 4 + +[TestingAndDebugging::RequireUseStrict] +equivalent_modules = MetaCPAN::Moose Mojo::Base Test::Routine + +[TestingAndDebugging::RequireUseWarnings] +equivalent_modules = MetaCPAN::Moose Mojo::Base Test::Routine + +[ValuesAndExpressions::ProhibitEmptyQuotes] +severity = 4 + +[ValuesAndExpressions::ProhibitInterpolationOfLiterals] +allow_if_string_contains_single_quote = 1 +allow = qq{} qq[] +severity = 4 + +[ValuesAndExpressions::ProhibitNoisyQuotes] +severity = 4 diff --git a/.perltidyrc b/.perltidyrc new file mode 100644 index 000000000..ab8fed578 --- /dev/null +++ b/.perltidyrc @@ -0,0 +1,17 @@ +--maximum-line-length=78 +--indent-columns=4 +--continuation-indentation=4 +--standard-error-output +--vertical-tightness=2 +--closing-token-indentation=0 +--paren-tightness=1 +--brace-tightness=1 +--square-bracket-tightness=1 +--block-brace-tightness=1 +--nospace-for-semicolon +--nooutdent-long-quotes +--want-break-before="% + - * / x != == >= <= =~ !~ < > | & = **= += *= &= <<= &&= -= /= |= >>= ||= //= .= %= ^= x=" +# Break a line after opening/before closing token. +--vertical-tightness=0 +--vertical-tightness-closing=0 +--weld-nested-containers diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..2948c9f24 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,101 @@ +ARG SLIM_BUILD +ARG MAYBE_BASE_BUILD=${SLIM_BUILD:+server-base-slim} +ARG BASE_BUILD=${MAYBE_BASE_BUILD:-server-base} + +################### Web Server Base +FROM metacpan/metacpan-base:main-20250531-090128 AS server-base +FROM metacpan/metacpan-base:main-20250531-090129-slim AS server-base-slim + +################### CPAN Prereqs +FROM server-base AS build-cpan-prereqs +SHELL [ "/bin/bash", "-euo", "pipefail", "-c" ] + +WORKDIR /app/ + +COPY cpanfile cpanfile.snapshot ./ +RUN \ + --mount=type=cache,target=/root/.perl-cpm,sharing=private \ +< + Copyright (C) 19yy + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 1, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) 19xx name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the +appropriate parts of the General Public License. Of course, the +commands you use may be called something other than `show w' and `show +c'; they could even be mouse-clicks or menu items--whatever suits your +program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + program `Gnomovision' (a program to direct compilers to make passes + at assemblers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +That's all there is to it! + + +--- The Artistic License 1.0 --- + +This software is Copyright (c) 2010 by Olaf Alders. + +This is free software, licensed under: + + The Artistic License 1.0 + +The Artistic License + +Preamble + +The intent of this document is to state the conditions under which a Package +may be copied, such that the Copyright Holder maintains some semblance of +artistic control over the development of the package, while giving the users of +the package the right to use and distribute the Package in a more-or-less +customary fashion, plus the right to make reasonable modifications. + +Definitions: + + - "Package" refers to the collection of files distributed by the Copyright + Holder, and derivatives of that collection of files created through + textual modification. + - "Standard Version" refers to such a Package if it has not been modified, + or has been modified in accordance with the wishes of the Copyright + Holder. + - "Copyright Holder" is whoever is named in the copyright or copyrights for + the package. + - "You" is you, if you're thinking about copying or distributing this Package. + - "Reasonable copying fee" is whatever you can justify on the basis of media + cost, duplication charges, time of people involved, and so on. (You will + not be required to justify it to the Copyright Holder, but only to the + computing community at large as a market that must bear the fee.) + - "Freely Available" means that no fee is charged for the item itself, though + there may be fees involved in handling the item. It also means that + recipients of the item may redistribute it under the same conditions they + received it. + +1. You may make and give away verbatim copies of the source form of the +Standard Version of this Package without restriction, provided that you +duplicate all of the original copyright notices and associated disclaimers. + +2. You may apply bug fixes, portability fixes and other modifications derived +from the Public Domain or from the Copyright Holder. A Package modified in such +a way shall still be considered the Standard Version. + +3. You may otherwise modify your copy of this Package in any way, provided that +you insert a prominent notice in each changed file stating how and when you +changed that file, and provided that you do at least ONE of the following: + + a) place your modifications in the Public Domain or otherwise make them + Freely Available, such as by posting said modifications to Usenet or an + equivalent medium, or placing the modifications on a major archive site + such as ftp.uu.net, or by allowing the Copyright Holder to include your + modifications in the Standard Version of the Package. + + b) use the modified Package only within your corporation or organization. + + c) rename any non-standard executables so the names do not conflict with + standard executables, which must also be provided, and provide a separate + manual page for each non-standard executable that clearly documents how it + differs from the Standard Version. + + d) make other distribution arrangements with the Copyright Holder. + +4. You may distribute the programs of this Package in object code or executable +form, provided that you do at least ONE of the following: + + a) distribute a Standard Version of the executables and library files, + together with instructions (in the manual page or equivalent) on where to + get the Standard Version. + + b) accompany the distribution with the machine-readable source of the Package + with your modifications. + + c) accompany any non-standard executables with their corresponding Standard + Version executables, giving the non-standard executables non-standard + names, and clearly documenting the differences in manual pages (or + equivalent), together with instructions on where to get the Standard + Version. + + d) make other distribution arrangements with the Copyright Holder. + +5. You may charge a reasonable copying fee for any distribution of this +Package. You may charge any fee you choose for support of this Package. You +may not charge a fee for this Package itself. However, you may distribute this +Package in aggregate with other (possibly commercial) programs as part of a +larger (possibly commercial) software distribution provided that you do not +advertise this Package as a product of your own. + +6. The scripts and library files supplied as input to or produced as output +from the programs of this Package do not automatically fall under the copyright +of this Package, but belong to whomever generated them, and may be sold +commercially, and may be aggregated with this Package. + +7. C or perl subroutines supplied by you and linked into this Package shall not +be considered part of this Package. + +8. The name of the Copyright Holder may not be used to endorse or promote +products derived from this software without specific prior written permission. + +9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF +MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +The End + diff --git a/README.md b/README.md index 2eccc6fec..eb3f0bdc5 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,149 @@ -A Web Service for the CPAN -========================== +# A Web Service for the CPAN + +[![CircleCI](https://circleci.com/gh/metacpan/metacpan-api.svg?style=svg)](https://circleci.com/gh/metacpan/metacpan-api) MetaCPAN aims to provide a free, open web service which provides metadata for CPAN modules. -REST API --------- +## REST API -MetaCPAN is based on ElasticSearch, so it provides a RESTful interface as well -as the option to create complex queries. [The -wiki](https://github.com/CPAN-API/cpan-api/wiki/Beta-API-docs) provides a good -starting point for REST access to MetaCPAN. +MetaCPAN is based on Elasticsearch, so it provides a RESTful interface as well +as the option to create complex queries. [The `docs/` +directory](https://github.com/metacpan/metacpan-api/blob/master/docs/API-docs.md) +provides a good starting point for REST access to MetaCPAN. -Expanding Your Author Info --------------------------- +## Expanding Your Author Info MetaCPAN allows authors to add custom metadata about themselves to the index. [Log in to MetaCPAN](https://metacpan.org/account/profile) to add more information about yourself. -Installing Your Own MetaCPAN: ---------------------------------------- +## Installing Your Own MetaCPAN -See the [installation](https://github.com/CPAN-API/cpan-api/wiki/Installation) -page in the wiki to start playing with your own MetaCPAN installation. +If you want to run MetaCPAN locally, we encourage you to start with +[metacpan-docker](https://github.com/metacpan/metacpan-docker). However, you +may still find some info here: -Contributing: -------------- +## Troubleshooting Elasticsearch -If you'd like to get involved, find us at #metacpan or irc.perl.org or join -our mailing list (see below) and let us know what you'd like to start working -on. +You can restart Elasticsearch (ES) manually if you need to troubleshoot. -IRC ---- +```sh +sudo service elasticsearch restart +``` -You can find us at #metacpan on irc.perl.org +If you are unable to access [[http://localhost:9200]] (give it a few seconds) +you should kill the Elasticsearch process and run it in foreground to see the +debug output + +```sh +sudo service elasticsearch stop +cd /opt/elasticsearch +sudo bin/elasticsearch -f +``` + +If you get a "Can't start up: not enough memory" error when trying to start +Elasticsearch, you likely need to update your JRE. On Ubuntu: + +```sh +# fixes "not enough memory" errors +sudo apt-get install openjdk-6-jre +``` + +(Note: If you intend to try indexing a full MiniCPAN, you may find that +Elasticsearch wants to use more open filehandles than your system allows by +default. [This script](https://gist.github.com/3230962) can be used to start ES +with the appropriate ulimit adjustment). + +## Run the test suite + +The test suite accesses Elasticsearch on port 9900. The developer VM should +have a dedicated test instance running in the background already, but if you +want to run it manually: + +```sh +cd /opt/elasticsearch +sudo bin/elasticsearch -f -Des.http.port=9900 -Des.cluster.name=testing +``` + +Then run the test suite: + +```sh +cd /home/metacpan/metacpan-api +./bin/prove t +``` + +The test suite has to pass all tests. + +## Create the ElasticSearch Index + +```sh +./bin/run bin/metacpan mapping --delete +``` + +`--delete` will drop all indices first to clear the index from test data. + +## Begin Indexing Your Modules + +```sh +./bin/run bin/metacpan release /path/to/cpan/authors/id/ +``` + +You should note that you can index either your CPAN mirror or a minicpan +mirror. You can even index just parts of a mirror: -IRC logs can be found here: -[http://irclog.perlgeek.de/metacpan/today](http://irclog.perlgeek.de/metacpan/today) -(Thanks to [Moritz Lenz](http://moritz.faui2k3.org/) for making this service -available) +```sh +./bin/run bin/metacpan release /path/to/cpan/authors/id/{A,B} +``` -Mailing List ------------- +## Tag the Latest Releases -Our mailing list is open to all: -[http://groups.google.com/group/cpan-api](http://groups.google.com/group/cpan-api) +```sh +./bin/run bin/metacpan latest --cpan /path/to/cpan/ +``` + +## Index Author Data + +```sh +./bin/run bin/metacpan author --cpan /path/to/cpan/ +``` + +Note that minicpan doesn't provide the 00whois.xml file which is used to +generate the index; you will have to download it manually (it is in the +authors/ directory) in order to index authors. + +```bash +wget -O /path/to/cpan/authors/00whois.xml cpan.cpantesters.org/authors/00whois.xml +``` + +It also doesn't include author.json files, so that data will also be missing +unless you get it from somewhere else. + +## Set Up Proxy in Front of ElasticSearch + +Start API server on port 5000 + +```sh +./bin/run plackup -p 5000 -r +``` + +This will start a single-threaded test server. If you need extra performance, +use `Starman` instead. + +## Notes + +For a full list of options: + +```sh +./bin/run bin/metacpan release --help +``` + +## Contributing + +If you'd like to get involved, find us at #metacpan on irc.perl.org or open an +issue on GitHub and let us know what you'd like to start working on. + +## IRC + +You can find us at #metacpan on irc.perl.org +Access it via [web interface](https://chat.mibbit.com/?channel=%23metacpan&server=irc.perl.org). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..0d2aab4de --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| `master` branch | :white_check_mark: | + +## Reporting a Vulnerability + +Please report all vulnerabilities by sending an email to noc@metacpan.org diff --git a/app.psgi b/app.psgi deleted file mode 120000 index 143d2a5cf..000000000 --- a/app.psgi +++ /dev/null @@ -1 +0,0 @@ -lib/MetaCPAN/Server.pm \ No newline at end of file diff --git a/app.psgi b/app.psgi new file mode 100644 index 000000000..184c2428c --- /dev/null +++ b/app.psgi @@ -0,0 +1,100 @@ +use strict; +use warnings; +use File::Basename (); +my $root_dir; + +BEGIN { + $root_dir = File::Basename::dirname(__FILE__); +} +use lib "$root_dir/lib"; + +use Config::ZOMG (); +use File::Path (); +use File::Spec (); +use Log::Log4perl (); +use Path::Tiny qw( path ); +use Plack::App::File (); +use Plack::App::Directory (); +use Plack::App::URLMap (); +use Plack::Util (); + +my $dev_mode; +my $config; + +BEGIN { + $dev_mode = $ENV{PLACK_ENV} && $ENV{PLACK_ENV} eq 'development'; + $config = Config::ZOMG->open( + name => 'MetaCPAN::Server', + path => $root_dir, + ); + + if ($dev_mode) { + $ENV{METACPAN_SERVER_DEBUG} = 1; + if ( !$ENV{EMAIL_SENDER_TRANSPORT} ) { + $ENV{EMAIL_SENDER_TRANSPORT} = 'Maildir'; + File::Path::mkpath( $ENV{EMAIL_SENDER_TRANSPORT_dir} + = "$root_dir/var/tmp/mail" ); + } + } + + my $log4perl_config + = File::Spec->rel2abs( $config->{log4perl_file} || 'log4perl.conf', + $root_dir ); + Log::Log4perl::init($log4perl_config); + + package MetaCPAN::Server::WarnHandler; ## no critic (Modules::RequireFilenameMatchesPackage) + Log::Log4perl->wrapper_register(__PACKAGE__); + my $logger = Log::Log4perl->get_logger; + $SIG{__WARN__} = sub { $logger->warn(@_) }; +} + +use MetaCPAN::Server (); + +STDERR->autoflush; + +# prevent output buffering when in Docker containers (e.g. in docker-compose) +if ( -e "/.dockerenv" and MetaCPAN::Server->log->isa('Catalyst::Log') ) { + STDOUT->autoflush; +} + +sub _add_headers { + my ( $app, $add_headers ) = @_; + sub { + Plack::Util::response_cb( + $app->(@_), + sub { + my $res = shift; + my ( $status, $headers ) = @$res; + if ( $status >= 200 && $status < 300 ) { + push @$headers, @$add_headers; + } + return $res; + } + ); + }; +} + +my $static + = Plack::App::Directory->new( + { root => path( $root_dir, 'root', 'static' ) } )->to_app; + +my $urlmap = Plack::App::URLMap->new; +$urlmap->map( + '/favicon.ico' => _add_headers( + Plack::App::File->new( + file => path( $root_dir, 'root', 'static', 'favicon.ico' ) + )->to_app, + [ + 'Cache-Control' => 'public, max-age=' . ( 60 * 60 * 24 ), + 'Surrogate-Control' => 'max-age=' . ( 60 * 60 * 24 * 365 ), + 'Surrogate-Key' => 'static', + ], + ) +); +$urlmap->map( '/static' => $static ); +if ( $ENV{PLACK_ENV} && $ENV{PLACK_ENV} eq 'development' ) { + $urlmap->map( '/v1' => MetaCPAN::Server->app ); +} +$urlmap->map( '/' => MetaCPAN::Server->app ); + +return $urlmap->to_app; diff --git a/bin/api.pl b/bin/api.pl new file mode 100755 index 000000000..40ffdc72e --- /dev/null +++ b/bin/api.pl @@ -0,0 +1,40 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +=head2 DESCRIPTION + +This is the API web server interface. + + # On vagrant VM + ./bin/run morbo bin/api.pl + +To run the api web server, run the following on one of the servers: + + # Run the daemon on a local port (tunnel to display on your browser) + ./bin/run bin/api.pl daemon + +Start Minion worker on vagrant: + + cd /home/vagrant/metacpan-api + ./bin/run bin/api.pl minion worker + +Get status on jobs and workers. + +On production: + + sh /home/metacpan/bin/metacpan-api-carton-exec bin/api.pl minion job -s + +On vagrant: + + cd /home/vagrant/metacpan-api + ./bin/run bin/api.pl minion job -s + +=cut + +use lib 'lib'; + +# Start command line interface for application +require Mojolicious::Commands; +Mojolicious::Commands->start_app('MetaCPAN::API'); diff --git a/bin/check_json.pl b/bin/check_json.pl deleted file mode 100755 index 9eee4effa..000000000 --- a/bin/check_json.pl +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env perl -# PODNAME: check_json.pl -use 5.010; - -use Data::Dumper; -use JSON::XS; - -foreach my $file ( @ARGV ) { - say "Processing $file"; - eval { - my $hash = decode_json( - do { local ( @ARGV, $/ ) = $file; <> } - ); - print Dumper( $hash ); - }; - - if ( $@ ) { say "\terror in $file: $@" } -} \ No newline at end of file diff --git a/bin/convert_authors.pl b/bin/convert_authors.pl deleted file mode 100644 index 5f7373a21..000000000 --- a/bin/convert_authors.pl +++ /dev/null @@ -1,90 +0,0 @@ -# PODNAME: foo - -use strict; -use warnings; -use JSON; -use File::Find; - -my @files; -find( - sub { - push( @files, $File::Find::name ); - }, - 'conf/authors' ); - -foreach my $file (@files) { - next unless ( -f $file ); - next if($file =~ /1/); - next unless($file =~ /\.json$/); - my $json; - { - local $/ = undef; - local *FILE; - open FILE, "<$file"; - $json = ; - close FILE - } - warn $file; - my $data = decode_json($json); - my ($author) = keys %$data; - ($data) = values %$data; - my $raw = { donation => [], - profile => [], }; - my %profiles = ( "delicious_username" => 'delicious', - "facebook_public_profile" => 'facebook', - "github_username" => 'github', - "linkedin_public_profile" => "linkedin", - "stackoverflow_public_profile" => 'stackoverflow', - "perlmonks_username" => 'perlmonks', - "twitter_username" => 'twitter', - "slideshare_url" => 'slideshare', - "youtube_channel_url" => 'youtube', - slashdot_username => 'slashdot', - "amazon_author_profile" => 'amazon', - aim => 'aim', - icq => 'icq', - jabber => 'jabber', - msn_messenger => 'msn_messenger', - "oreilly_author_profile" => 'oreilly', - slideshare_username => 'slideshare', - stumbleupon_profile => 'stumbleupon', - xing_public_profile => 'xing', - ACT_id => 'act', - irc_nick => 'irc', - irc_nickname => 'irc' ); - - while ( my ( $k, $v ) = each %profiles ) { - next unless ( my $value = delete $data->{$k} ); - $value =~ s/^.*\///; - push( @{ $raw->{profile} }, - { name => $v, - id => $value - } ); - } - - if ( my $pp = delete $data->{paypal_address} ) { - delete $data->{accepts_donations}; - push( @{ $raw->{donation} }, - { id => $pp, - name => 'paypal' - } ); - } - - if ( $data->{blog_url} ) { - $raw->{blog} = [ - { url => delete $data->{blog_url}, - feed => delete $data->{blog_feed} } ]; - } - delete $data->{perlmongers} if ( ref $data->{perlmongers} ); - if ( $data->{perlmongers} ) { - $raw->{perlmongers} = { name => delete $data->{perlmongers}, - url => delete $data->{perlmongers_url}, }; - } - $raw->{$_} = delete $data->{$_} - for (qw(city country email region website openid)); - unlink $file; - (my $base = $file) =~ s/^(.*)\/.*?$/$1/; - open FILE, '>', "$base/author-1.0.json"; - print FILE JSON->new->pretty->encode( $raw ); - close FILE; -} diff --git a/bin/cpantesters_api_file_for_testing b/bin/cpantesters_api_file_for_testing new file mode 100755 index 000000000..f7eb90d22 --- /dev/null +++ b/bin/cpantesters_api_file_for_testing @@ -0,0 +1,59 @@ +#!/bin/bash + +cd `dirname "$0"` +cd .. + +url=http://api.cpantesters.org/v3/release +in=t/var/tmp/cpantesters-release-api.json +out=t/var/cpantesters-release-api-fake.json + +download_original () { + test -s "$in" || wget -O "$in" "$url" +} + +append_json () { + perl -MJSON::PP -e' + $file = shift; + $all = -e $file ? decode_json( + do { local $/; open $fh, "<", $file; <$fh> } + ) : []; + $add = decode_json( join "", ); + push @$all, $add; + open $fh, ">", $file; + print { $fh } encode_json( $all ) ' $out +} + +collect_dist () { + local dist="$1" version="$2" + jq '.[] | select( .dist == $dist and .version == $version )' \ + --arg dist "$dist" --arg version "$version" $in \ + | append_json +} + +fake_dist () { + echo "{ \"dist\": \"$1\", \"version\": \"$2\", \"pass\": $3, \"fail\": $4, \ + \"na\": $5, \"unknown\": $6 }" | append_json; +} + +populate_file () { + rm -f "$out" + + # Get test cases from real data. + collect_dist 'Devel-GoFaster' '0.000' + collect_dist 'P' '1.0.20' + collect_dist 'IPsonar' '0.29' + collect_dist 'weblint' '++-1.15' + collect_dist 'WWW-Tumblr' '' + + # Add records for our fake dists. + fake_dist 'Some' '1.00-TRIAL' 4 3 2 1 +} + +if [ !-x $( which jq ) ]; then + echo "ERROR: jq(1) required for this script" + exit 1 +fi + +download_original +populate_file + diff --git a/bin/cpantesters_mini_db_for_testing b/bin/cpantesters_mini_db_for_testing new file mode 100755 index 000000000..8b279c3d0 --- /dev/null +++ b/bin/cpantesters_mini_db_for_testing @@ -0,0 +1,63 @@ +#!/bin/bash + +cd `dirname "$0"` +cd .. + +url=http://devel.cpantesters.org/release/release.db.bz2 +in=t/var/tmp/cpantesters-release.db +out=t/var/cpantesters-release-fake.db +table=release + +download_original () { + test -s "$in" || \ + wget -O "$in.bz2" "$url" + test -f "$in.bz2" && \ + bunzip2 "$in.bz2" + + rm -f "$out" "$out.bz2" +} + +finish () { + # Compress the db like cpantesters does. + bzip2 "$out" +} + +sqlout () { sqlite3 "$out"; } +sql () { + sqlite3 "$in" | sqlout +} + +dist_version () { + local dist="$1" version="$2" +cat <new( $current_dir, '..', 'conf' ); -my @files = File::Find::Rule->file->name( '*.json' )->in( $author_dir ); - -my %fields; - -foreach my $file ( @files ) { - warn "Processing $file"; - my $hash; - - eval { - $hash = decode_json( do { local( @ARGV, $/ ) = $file; <> } ); - } or print "\terror in $file: $@"; - - while ( my ($author, $info) = each %{$hash} ) { - my @local_fields = keys %{$info}; - @fields{@local_fields} = @local_fields; - } -} - -print $_ for sort keys %fields; diff --git a/bin/install-precious b/bin/install-precious new file mode 100755 index 000000000..0d712a470 --- /dev/null +++ b/bin/install-precious @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# This is for installing precious and other 3rd party libs needed for linting +# in CI + +set -euo pipefail + +if [ -z "${1:-}" ]; then + echo "usage: ./bin/install-precious /path/to/bin/dir" + exit 1 +fi + +TARGET=$1 +export TARGET + +TARGET=$1 +export TARGET + +curl --silent --location \ + https://raw.githubusercontent.com/houseabsolute/ubi/master/bootstrap/bootstrap-ubi.sh | + sh + +ubi --project houseabsolute/omegasort --in "$TARGET" +ubi --project houseabsolute/precious --in "$TARGET" diff --git a/bin/metacpan b/bin/metacpan index d01ec6f6a..e57c34fdb 100755 --- a/bin/metacpan +++ b/bin/metacpan @@ -1,5 +1,4 @@ #!/usr/bin/env perl -# PODNAME: metadbic =head1 SYNOPSIS @@ -7,7 +6,7 @@ bin/metacpan release /path/to/cpan/authors/id/ bin/metacpan release /path/to/cpan/authors/id/{A,B} - bin/metacpan release /path/to/cpan/authors/id/D/DO/DOY/Try-Tiny-0.09.tar.gz + bin/metacpan release /path/to/cpan/authors/id/D/DO/DOY/Try-Tiny-0.09.tar.gz bin/metacpan latest bin/metacpan server --cpan /path/to/cpan/ @@ -15,8 +14,10 @@ use strict; use warnings; -use FindBin; +use FindBin (); use lib "$FindBin::RealBin/../lib"; -use MetaCPAN::Script::Runner; +use MetaCPAN::Script::Runner (); MetaCPAN::Script::Runner->run; + +exit $MetaCPAN::Script::Runner::EXIT_CODE; diff --git a/bin/mirror_cpan_for_developers.pl b/bin/mirror_cpan_for_developers.pl new file mode 100755 index 000000000..7a8d4de10 --- /dev/null +++ b/bin/mirror_cpan_for_developers.pl @@ -0,0 +1,15 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +# This script is only needed if you are developing metacpan, +# on the live servers we use File::Rsync::Mirror::Recent +# https://github.com/metacpan/metacpan-puppet/tree/master/modules/rrrclient + +use CPAN::Mini; + +CPAN::Mini->update_mirror( + remote => 'http://www.cpan.org/', + local => "/home/metacpan/CPAN", + log_level => 'warn', +); diff --git a/bin/munin/monitor_minion_queue.pl b/bin/munin/monitor_minion_queue.pl new file mode 100755 index 000000000..91a83e6ff --- /dev/null +++ b/bin/munin/monitor_minion_queue.pl @@ -0,0 +1,58 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +# Munin runs this as metacpan user, but with root's env +# it's only for production so path is hard coded + +my $config_mode = 0; +$config_mode = 1 if $ARGV[0] && $ARGV[0] eq 'config'; + +if ($config_mode) { + + # Dump this (though we supported dynamic below) so it's faster + print <<'EOF'; +graph_title Minion Queue stats +graph_vlabel count +graph_category metacpan_api +graph_info What's happening in the Minion queue +workers_inactive.label Inactive workers +workers_active.label Active workers +jobs_inactive.label Inactive jobs +jobs_active.label Active jobs +jobs_failed.label Failed jobs +jobs_finished.label Finished jobs +EOF + + exit; +} + +# Get the stats +my $stats_report + = `/home/metacpan/bin/metacpan-api-carton-exec bin/queue.pl minion job -s`; + +my @lines = split( "\n", $stats_report ); + +for my $line (@lines) { + my ( $label, $num ) = split ':', $line; + + $num =~ s/\D//g; + + my $key = lc($label); # Was 'Inactive jobs' + + # Swap type and status around so idle_jobs becomes jobs_idle + $key =~ s/(\w+)\s+(\w+)/$2_$1/g; + + if ($config_mode) { + + # config + print "${key}.label $label\n"; + + } + else { + # results + print "${key}.value $num\n" if $num; + } + +} diff --git a/bin/queue.pl b/bin/queue.pl new file mode 120000 index 000000000..5474dbc6e --- /dev/null +++ b/bin/queue.pl @@ -0,0 +1 @@ +api.pl \ No newline at end of file diff --git a/bin/run b/bin/run new file mode 100755 index 000000000..3c4ded42a --- /dev/null +++ b/bin/run @@ -0,0 +1,10 @@ +#!/bin/sh + +# Use the puppet-installed wrapper to set up the env properly. +wrapper=$HOME/bin/metacpan-api-carton +test -x $wrapper && \ +exec $wrapper exec -- "$@" + +# If the wrapper doesn't exist, just try it with plain carton. +cd "`dirname "$0"`"/.. +exec carton exec -- "$@" diff --git a/bin/wait-for-open b/bin/wait-for-open new file mode 100755 index 000000000..a9729260e --- /dev/null +++ b/bin/wait-for-open @@ -0,0 +1,16 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +my $server = shift; + +my $timeout = 30; +while ( $timeout-- ) { + if ( !system "curl -s '$server' 2>/dev/null 1>&2" ) { + exit 0; + } + sleep 1; +} + +print STDERR "Timed out starting elasticsearch!\n"; +exit 1; diff --git a/bin/write_config_json b/bin/write_config_json deleted file mode 100644 index 9968d6829..000000000 --- a/bin/write_config_json +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/perl -# takes the root directory of an extracted distribution and outputs a JSON file -# suitable for CPAN::Faker to STDOUT -use strictures 1; -use JSON; -use YAML; - -use IO::All; -use Email::Address; -use File::Find; -use File::Spec; -use Path::Class; - -my ($dir) = @ARGV; - -my $meta_data; -if(-e "$dir/META.yml") { - $meta_data = YAML::LoadFile("$dir/META.yml"); -} elsif(-e "$dir/META.json") { - $meta_data = JSON::decode_json(io("$dir/META.json")->all); -} else { - die "no meta file"; -} - -my $authors = $meta_data->{author}; -my @authors = map { my ($addr) = Email::Address->parse($_); $addr->name } @$authors; - -my $files; -File::Find::find( - { - no_chdir => 1, - wanted => sub { - return unless -f; - push @$files, { - file => File::Spec->abs2rel($File::Find::name, dir($dir)), - content => io($_)->all, - } - }, - }, - $dir -); - -my $output = { - name => $meta_data->{name}, - version => $meta_data->{version}, - abstract => $meta_data->{abstract}, - X_Module_Faker => { - cpan_author => [ @authors ], - append => [ - $files - ] - }, -}; - -print JSON->new->pretty->encode($output); - -__DATA__ -{ - "name": "MetaFile-Both", - "abstract": "A dist with META.yml and META.json", - "version": 1.1, - "X_Module_Faker": { - "cpan_author": "LOCAL", - "append": [ { - "file": "lib/MetaFile/Both.pm", - "content": "package MetaFile::Both;\n\n=head1 NAME\n\nMetaFile::Both - abstract" - }, - { - "file": "META.json", - "content": "{\"meta-spec\":{\"version\":2,\"url\":\"http://search.cpan.org/perldoc?CPAN::Meta::Spec\"},\"generated_by\":\"hand\",\"version\":1.1,\"name\":\"MetaFile-Both\",\"dynamic_config\":0,\"author\":\"LOCAL\",\"license\":\"unknown\",\"abstract\":\"A dist with META.yml and META.json\",\"release_status\":\"stable\",\"x_meta_file\":\"json\"}" - }, - { - "file": "t/foo.t", - "content": "use Test::More;" - } ] - } -} - ---- -name: SignedModule -version: 1.1 -abstract: A signed dist -author: - - LOCAL -generated_by: Module::Faker version -license: unknown -meta-spec: - url: http://module-build.sourceforge.net/META-spec-v1.3.html - version: 1.3 diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..be10c0793 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,16 @@ +--- +comment: + layout: 'diff, files' + behavior: default + require_changes: true # if true: only post the comment if coverage changes + require_base: false # [true :: must have a base report to post] + require_head: true # [true :: must have a head report to post] + hide_project_coverage: false # [true :: only show coverage on the git diff] +coverage: + status: + patch: + default: + threshold: 1% + project: + default: + threshold: 1% diff --git a/cpanfile b/cpanfile new file mode 100644 index 000000000..dd1d4513f --- /dev/null +++ b/cpanfile @@ -0,0 +1,166 @@ +use strict; +use warnings; + +requires 'perl', '5.010'; + +requires 'Archive::Any', '0.0946'; +requires 'Archive::Tar', '2.40'; +requires 'Authen::SASL', '2.16'; # for Email::Sender::Transport::SMTP +requires 'Catalyst', '5.90128'; +requires 'Catalyst::Action::RenderView', '0.16'; +requires 'Catalyst::Controller::REST', '1.21'; +requires 'Catalyst::Plugin::Authentication'; +requires 'Catalyst::Plugin::Session', '0.43'; +requires 'Catalyst::Plugin::Session::State::Cookie'; +requires 'Catalyst::Plugin::Session::Store'; +requires 'Catalyst::Plugin::Static::Simple'; +requires 'Catalyst::View::JSON', '0.37'; +requires 'CatalystX::Fastly::Role::Response', '0.06'; +requires 'CHI', '0.61'; +requires 'Config::General', '2.63'; +requires 'Config::ZOMG', '1.000000'; +requires 'Const::Fast'; +requires 'CPAN::DistnameInfo', '0.12'; +requires 'Cpanel::JSON::XS', '4.32'; +requires 'CPAN::Meta', '2.150005'; # Avoid issues with List::Util dep under carton install. +requires 'CPAN::Meta::Requirements', '2.140'; +requires 'CPAN::Meta::YAML', '0.018'; +requires 'CPAN::Repository::Perms'; +requires 'Cwd'; +requires 'Data::Dumper'; +requires 'Data::Visitor::Callback'; +requires 'DateTime', '1.54'; +requires 'DateTime::Format::ISO8601'; +requires 'DBD::SQLite', '1.66'; +requires 'DBI', '1.643'; +requires 'Digest::MD5'; +requires 'Digest::SHA'; +requires 'ElasticSearchX::Model', '2.0.1'; +requires 'Email::Sender::Simple'; +requires 'Email::Simple'; +requires 'Email::Valid', '1.203'; +requires 'Encode', '3.17'; +requires 'Encoding::FixLatin'; +requires 'Encoding::FixLatin::XS'; +requires 'EV'; +requires 'Exporter', '5.74'; +requires 'File::Basename'; +requires 'File::Copy'; +requires 'File::Find'; +requires 'File::Find::Rule'; +requires 'File::Find::Rule::Perl'; +requires 'File::Spec'; +requires 'File::Spec::Functions'; +requires 'File::pushd'; +requires 'File::stat'; +requires 'File::Temp'; +requires 'FindBin'; +requires 'Getopt::Long::Descriptive', '0.103'; +requires 'Gravatar::URL'; +requires 'Hash::Merge::Simple'; +requires 'HTML::Entities'; +requires 'HTTP::Request::Common', '6.36'; +requires 'IO::Prompt::Tiny'; +requires 'IO::Uncompress::Bunzip2', '2.106'; +requires 'IO::Zlib'; +requires 'IPC::Run3', '0.048'; +requires 'List::Util', '1.62'; +requires 'Log::Any::Adapter'; +requires 'Log::Any::Adapter::Log4perl'; +requires 'Log::Contextual'; +requires 'Log::Dispatch'; +requires 'Log::Dispatch::Syslog'; +requires 'Log::Log4perl'; +requires 'Log::Log4perl::Appender::ScreenColoredLevels'; +requires 'Log::Log4perl::Catalyst'; +requires 'Log::Log4perl::Layout::JSON'; +requires 'LWP::Protocol::https'; +requires 'LWP::UserAgent', '6.66'; +requires 'MetaCPAN::Moose'; +requires 'MetaCPAN::Pod::HTML' => '0.004000'; +requires 'MetaCPAN::Role', '1.00'; +requires 'MIME::Base64', '3.15'; +requires 'Minion', '9.03'; +requires 'Minion::Backend::SQLite'; +requires 'Module::Load'; +requires 'Module::Metadata', '1.000038'; +requires 'Module::Pluggable'; +requires 'Module::Runtime'; +requires 'Mojolicious::Plugin::MountPSGI', '0.14'; +requires 'Mojolicious::Plugin::OpenAPI'; +requires 'Mojolicious::Plugin::Web::Auth', '0.17'; +requires 'Mojo::Pg', '4.08'; +requires 'Moose', '2.2201'; +requires 'MooseX::Attribute::Deflator', '2.1.5'; +requires 'MooseX::Fastly::Role', '0.02'; +requires 'MooseX::Getopt', '0.71'; +requires 'MooseX::Getopt::Dashes'; +requires 'MooseX::Getopt::OptionTypeMap'; +requires 'MooseX::StrictConstructor'; +requires 'MooseX::Types'; +requires 'MooseX::Types::ElasticSearch', '0.0.4'; +requires 'MooseX::Types::Moose'; +requires 'Mozilla::CA', '20211001'; +requires 'namespace::autoclean'; +requires 'Net::Fastly', '1.12'; +requires 'Net::GitHub::V4'; +requires 'Parse::CPAN::Packages::Fast', '0.09'; +requires 'Parse::PMFile', '0.43'; +requires 'Path::Iterator::Rule', '>=1.011'; +requires 'PAUSE::Permissions', '0.17'; +requires 'PerlIO::gzip'; +requires 'Plack', '1.0048'; +requires 'Plack::App::Directory'; +requires 'Plack::Middleware::ReverseProxy'; +requires 'Plack::Middleware::Session'; +requires 'Plack::Session::Store'; +requires 'Pod::Markdown', '3.300'; +requires 'Pod::Text', '4.14'; +requires 'Ref::Util'; +requires 'Safe', '2.35'; # bug fixes (used by Parse::PMFile) +requires 'Scalar::Util', '1.62'; # Moose +requires 'Search::Elasticsearch' => '8.12'; +requires 'Search::Elasticsearch::Client::2_0' => '6.81'; +requires 'Throwable::Error'; +requires 'Term::Size::Any'; # for Catalyst +requires 'Text::CSV_XS'; +requires 'Try::Tiny', '0.30'; +requires 'Type::Tiny', '2.000001'; +requires 'Types::Path::Tiny'; +requires 'Types::URI'; +requires 'Twitter::API', '1.0006'; +requires 'URI', '5.10'; +requires 'version', '0.9929'; +requires 'XML::XPath'; +requires 'YAML::XS', '0.83'; # Mojolicious::Plugin::OpenAPI YAML loading + +# test requirements +on test => sub { + requires 'CPAN::Faker', '0.011'; + requires 'Devel::Confess'; + requires 'HTTP::Cookies', '6.10'; + requires 'MetaCPAN::Client', '2.029000'; + requires 'Module::Faker', '== 0.017'; + requires 'Module::Faker::Dist', '== 0.017'; + requires 'OrePAN2', '0.48'; + requires 'Test::Deep'; + requires 'Test::Fatal'; + requires 'Test::Harness', '3.44'; # Contains App::Prove + requires 'Test::More', '1.302190'; + requires 'Test::RequiresInternet'; + requires 'Test::Routine', '0.012'; + requires 'Test::Vars', '0.015'; +}; + +# author requirements +on develop => sub { + requires 'App::perlimports'; + requires 'Perl::Critic', '0.140'; + requires 'Perl::Tidy' => '== 20240511'; + requires 'PPI', '1.274'; # Perl::Critic + requires 'PPIx::QuoteLike', '0.022'; # Perl::Critic + requires 'PPIx::Regexp', '0.085'; # Perl::Critic + requires 'String::Format', '1.18'; # Perl::Critic + requires 'Devel::Cover'; + requires 'Devel::Cover::Report::Codecovbash'; +}; diff --git a/cpanfile.forced b/cpanfile.forced new file mode 100644 index 000000000..47023795c --- /dev/null +++ b/cpanfile.forced @@ -0,0 +1,12 @@ +# transitive deps +# Not used directly, but they need to be explicitly listed to ensure they are +# in our cpanfile.snapshot at appropriate versions. Either for older perl +# versions, or unpredictable dynamic deps. These will be installed using a +# different process to ensure they are present in the snapshot, even if they +# would be satisfied by core. +requires 'CPAN::Meta', '2.141520'; +requires 'Devel::PPPort', '3.62'; # for older perls +requires 'ExtUtils::MakeMaker', '7.76'; +requires 'version', '0.9929'; # for older perls +requires 'Module::Signature', '0.90'; +requires 'Pod::Parser', '1.67'; # for newer perls diff --git a/cpanfile.snapshot b/cpanfile.snapshot new file mode 100644 index 000000000..c3272cc2c --- /dev/null +++ b/cpanfile.snapshot @@ -0,0 +1,8353 @@ +# carton snapshot format: version 1.0 +DISTRIBUTIONS + Algorithm-Diff-1.201 + pathname: R/RJ/RJBS/Algorithm-Diff-1.201.tar.gz + provides: + Algorithm::Diff 1.201 + Algorithm::Diff::_impl 1.201 + requirements: + ExtUtils::MakeMaker 0 + Any-URI-Escape-0.01 + pathname: P/PH/PHRED/Any-URI-Escape-0.01.tar.gz + provides: + Any::URI::Escape 0.01 + requirements: + ExtUtils::MakeMaker 0 + URI::Escape 0 + Apache-LogFormat-Compiler-0.36 + pathname: K/KA/KAZEBURO/Apache-LogFormat-Compiler-0.36.tar.gz + provides: + Apache::LogFormat::Compiler 0.36 + requirements: + Module::Build::Tiny 0.035 + POSIX 0 + POSIX::strftime::Compiler 0.30 + Time::Local 0 + perl 5.008001 + App-perlimports-0.000056 + pathname: O/OA/OALDERS/App-perlimports-0.000056.tar.gz + provides: + App::perlimports 0.000056 + App::perlimports::Annotations 0.000056 + App::perlimports::CLI 0.000056 + App::perlimports::Config 0.000056 + App::perlimports::Document 0.000056 + App::perlimports::ExportInspector 0.000056 + App::perlimports::Include 0.000056 + App::perlimports::Role::Logger 0.000056 + App::perlimports::Sandbox 0.000056 + requirements: + Capture::Tiny 0 + Class::Inspector 1.36 + Cpanel::JSON::XS 0 + Data::Dumper 0 + Data::UUID 0 + ExtUtils::MakeMaker 0 + File::Basename 0 + File::XDG 1.01 + Getopt::Long::Descriptive 0 + List::Util 0 + Log::Dispatch 2.70 + Memoize 0 + Module::Runtime 0 + Moo 0 + Moo::Role 0 + MooX::StrictConstructor 0 + PPI 1.276 + PPI::Document 0 + PPIx::Utils::Classification 0 + Path::Iterator::Rule 0 + Path::Tiny 0 + Perl::Tidy 20220613 + Pod::Usage 0 + Ref::Util 0 + Scalar::Util 0 + Sereal::Decoder 0 + Sereal::Encoder 0 + Sub::HandlesVia 0 + Symbol::Get 0.10 + TOML::Tiny 0.16 + Text::Diff 0 + Text::SimpleTable::AutoWidth 0 + Try::Tiny 0 + Types::Standard 0 + feature 0 + perl v5.18.0 + strict 0 + utf8 0 + warnings 0 + Archive-Any-0.0946 + pathname: O/OA/OALDERS/Archive-Any-0.0946.tar.gz + provides: + Archive::Any 0.0946 + Archive::Any::Plugin 0.0946 + Archive::Any::Plugin::Tar 0.0946 + Archive::Any::Plugin::Zip 0.0946 + Archive::Any::Tar 0.0946 + Archive::Any::Zip 0.0946 + requirements: + Archive::Tar 0 + Archive::Zip 0 + Cwd 0 + ExtUtils::MakeMaker 0 + File::MMagic 0 + File::Spec::Functions 0 + MIME::Types 0 + Module::Find 0 + base 0 + perl 5.006 + strict 0 + warnings 0 + Archive-Any-Create-0.03 + pathname: M/MI/MIYAGAWA/Archive-Any-Create-0.03.tar.gz + provides: + Archive::Any::Create 0.03 + Archive::Any::Create::Tar undef + Archive::Any::Create::Zip undef + requirements: + Archive::Tar 0 + Archive::Zip 0 + Exception::Class 0 + ExtUtils::MakeMaker 6.30 + IO::Zlib 0 + UNIVERSAL::require 0 + Archive-Extract-0.88 + pathname: B/BI/BINGOS/Archive-Extract-0.88.tar.gz + provides: + Archive::Extract 0.88 + requirements: + ExtUtils::MakeMaker 0 + File::Basename 0 + File::Path 0 + File::Spec 0.82 + IPC::Cmd 0.64 + Locale::Maketext::Simple 0 + Module::Load::Conditional 0.66 + Params::Check 0.07 + Test::More 0 + if 0 + Archive-Tar-3.04 + pathname: B/BI/BINGOS/Archive-Tar-3.04.tar.gz + provides: + Archive::Tar 3.04 + Archive::Tar::Constant 3.04 + Archive::Tar::File 3.04 + requirements: + Compress::Zlib 2.015 + ExtUtils::MakeMaker 0 + File::Spec 0.82 + IO::Compress::Base 2.015 + IO::Compress::Bzip2 2.015 + IO::Compress::Gzip 2.015 + IO::Zlib 1.01 + Test::Harness 2.26 + Test::More 0 + perl 5.00503 + Archive-Zip-1.68 + pathname: P/PH/PHRED/Archive-Zip-1.68.tar.gz + provides: + Archive::Zip 1.68 + Archive::Zip::Archive 1.68 + Archive::Zip::BufferedFileHandle 1.68 + Archive::Zip::DirectoryMember 1.68 + Archive::Zip::FileMember 1.68 + Archive::Zip::Member 1.68 + Archive::Zip::MemberRead 1.68 + Archive::Zip::MockFileHandle 1.68 + Archive::Zip::NewFileMember 1.68 + Archive::Zip::StringMember 1.68 + Archive::Zip::Tree 1.68 + Archive::Zip::ZipFileMember 1.68 + requirements: + Compress::Raw::Zlib 2.017 + Encode 0 + ExtUtils::MakeMaker 0 + File::Basename 0 + File::Copy 0 + File::Find 0 + File::Path 0 + File::Spec 0.80 + File::Temp 0 + IO::File 0 + IO::Handle 0 + IO::Seekable 0 + Time::Local 0 + perl 5.006 + Authen-SASL-2.1800 + pathname: E/EH/EHUELS/Authen-SASL-2.1800.tar.gz + provides: + Authen::SASL 2.1800 + Authen::SASL::CRAM_MD5 2.1800 + Authen::SASL::EXTERNAL 2.1800 + Authen::SASL::Perl 2.1800 + Authen::SASL::Perl::ANONYMOUS 2.1800 + Authen::SASL::Perl::CRAM_MD5 2.1800 + Authen::SASL::Perl::DIGEST_MD5 2.1800 + Authen::SASL::Perl::EXTERNAL 2.1800 + Authen::SASL::Perl::GSSAPI 2.1800 + Authen::SASL::Perl::LOGIN 2.1800 + Authen::SASL::Perl::OAUTHBEARER 2.1800 + Authen::SASL::Perl::PLAIN 2.1800 + Authen::SASL::Perl::XOAUTH2 2.1800 + requirements: + Digest::HMAC_MD5 0 + Digest::MD5 0 + ExtUtils::MakeMaker 0 + perl 5.014000 + B-Hooks-EndOfScope-0.28 + pathname: E/ET/ETHER/B-Hooks-EndOfScope-0.28.tar.gz + provides: + B::Hooks::EndOfScope 0.28 + B::Hooks::EndOfScope::PP 0.28 + B::Hooks::EndOfScope::XS 0.28 + requirements: + ExtUtils::MakeMaker 0 + Hash::Util::FieldHash 0 + Module::Implementation 0.05 + Scalar::Util 0 + Sub::Exporter::Progressive 0.001006 + Text::ParseWords 0 + Tie::Hash 0 + Variable::Magic 0.48 + perl 5.006001 + strict 0 + warnings 0 + B-Hooks-OP-Check-0.22 + pathname: E/ET/ETHER/B-Hooks-OP-Check-0.22.tar.gz + provides: + B::Hooks::OP::Check 0.22 + requirements: + DynaLoader 0 + ExtUtils::Depends 0.302 + ExtUtils::MakeMaker 0 + parent 0 + perl 5.008001 + strict 0 + warnings 0 + B-Keywords-1.27 + pathname: R/RU/RURBAN/B-Keywords-1.27.tar.gz + provides: + B::Keywords 1.27 + requirements: + B 0 + ExtUtils::MakeMaker 0 + CGI-Simple-1.281 + pathname: M/MA/MANWAR/CGI-Simple-1.281.tar.gz + provides: + CGI::Simple 1.281 + CGI::Simple::Cookie 1.281 + CGI::Simple::Standard 1.281 + CGI::Simple::Util 1.281 + requirements: + ExtUtils::MakeMaker 0 + CGI-Struct-1.21 + pathname: F/FU/FULLERMD/CGI-Struct-1.21.tar.gz + provides: + CGI::Struct 1.21 + requirements: + ExtUtils::MakeMaker 0 + Storable 0 + Test::Deep 0 + Test::More 0 + CHI-0.61 + pathname: A/AS/ASB/CHI-0.61.tar.gz + provides: + CHI 0.61 + CHI::CacheObject 0.61 + CHI::Driver 0.61 + CHI::Driver::Base::CacheContainer 0.61 + CHI::Driver::CacheCache 0.61 + CHI::Driver::FastMmap 0.61 + CHI::Driver::File 0.61 + CHI::Driver::Memory 0.61 + CHI::Driver::Metacache 0.61 + CHI::Driver::Null 0.61 + CHI::Driver::RawMemory 0.61 + CHI::Driver::Role::HasSubcaches 0.61 + CHI::Driver::Role::IsSizeAware 0.61 + CHI::Driver::Role::IsSubcache 0.61 + CHI::Stats 0.61 + requirements: + Carp::Assert 0.20 + Class::Load 0 + Data::UUID 0 + Digest::JHash 0 + Digest::MD5 0 + ExtUtils::MakeMaker 0 + File::Spec 0.80 + Hash::MoreUtils 0 + JSON::MaybeXS 1.003003 + List::MoreUtils 0.13 + Log::Any 0.08 + Moo 1.003 + MooX::Types::MooseLike 0.23 + MooX::Types::MooseLike::Base 0 + MooX::Types::MooseLike::Numeric 0 + Storable 0 + String::RewritePrefix 0 + Task::Weaken 0 + Time::Duration 1.06 + Time::Duration::Parse 0.03 + Time::HiRes 1.30 + Try::Tiny 0.05 + CPAN-Checksums-2.14 + pathname: A/AN/ANDK/CPAN-Checksums-2.14.tar.gz + provides: + CPAN::Checksums 2.14 + requirements: + Compress::Bzip2 0 + Compress::Zlib 0 + Data::Compare 0 + Data::Dumper 0 + Digest::MD5 2.36 + Digest::SHA 0 + DirHandle 0 + ExtUtils::MakeMaker 0 + File::Spec 0 + File::Temp 0 + IO::File 1.14 + CPAN-DistnameInfo-0.12 + pathname: G/GB/GBARR/CPAN-DistnameInfo-0.12.tar.gz + provides: + CPAN::DistnameInfo 0.12 + requirements: + ExtUtils::MakeMaker 0 + Test::More 0 + CPAN-Faker-0.012 + pathname: R/RJ/RJBS/CPAN-Faker-0.012.tar.gz + provides: + CPAN::Faker 0.012 + requirements: + CPAN::Checksums 0 + Compress::Zlib 0 + Cwd 0 + Data::Section 0 + ExtUtils::MakeMaker 6.78 + File::Find 0 + File::Next 0 + File::Path 0 + File::Spec 0 + Getopt::Long::Descriptive 0 + IO::Compress::Gzip 0 + Module::Faker::Dist 0.015 + Moose 0 + Sort::Versions 0 + Text::Template 0 + perl 5.014000 + strict 0 + warnings 0 + CPAN-Meta-2.150010 + pathname: D/DA/DAGOLDEN/CPAN-Meta-2.150010.tar.gz + provides: + CPAN::Meta 2.150010 + CPAN::Meta::Converter 2.150010 + CPAN::Meta::Feature 2.150010 + CPAN::Meta::History 2.150010 + CPAN::Meta::Merge 2.150010 + CPAN::Meta::Prereqs 2.150010 + CPAN::Meta::Spec 2.150010 + CPAN::Meta::Validator 2.150010 + Parse::CPAN::Meta 2.150010 + requirements: + CPAN::Meta::Requirements 2.121 + CPAN::Meta::YAML 0.011 + Carp 0 + Encode 0 + Exporter 0 + ExtUtils::MakeMaker 6.17 + File::Spec 0.80 + JSON::PP 2.27300 + Scalar::Util 0 + perl 5.008001 + strict 0 + version 0.88 + warnings 0 + CPAN-Meta-Requirements-2.143 + pathname: R/RJ/RJBS/CPAN-Meta-Requirements-2.143.tar.gz + provides: + CPAN::Meta::Requirements 2.143 + CPAN::Meta::Requirements::Range 2.143 + requirements: + B 0 + Carp 0 + ExtUtils::MakeMaker 6.17 + perl 5.010000 + strict 0 + version 0.88 + warnings 0 + CPAN-Meta-YAML-0.020 + pathname: E/ET/ETHER/CPAN-Meta-YAML-0.020.tar.gz + provides: + CPAN::Meta::YAML 0.020 + requirements: + B 0 + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 6.17 + Fcntl 0 + Scalar::Util 0 + perl 5.008001 + strict 0 + warnings 0 + CPAN-Repository-0.010 + pathname: O/OA/OALDERS/CPAN-Repository-0.010.tar.gz + provides: + CPAN::Repository 0.010 + CPAN::Repository::Mailrc 0.010 + CPAN::Repository::Packages 0.010 + CPAN::Repository::Perms 0.010 + CPAN::Repository::Role::File 0.010 + requirements: + DateTime 0.72 + DateTime::Format::Epoch 0.13 + DateTime::Format::RFC3339 0 + Dist::Data 0.002 + ExtUtils::MakeMaker 0 + File::Path 2.08 + File::Spec::Functions 3.33 + IO::File 1.14 + IO::Zlib 1.10 + Moo 0.009013 + Cache-LRU-0.04 + pathname: K/KA/KAZUHO/Cache-LRU-0.04.tar.gz + provides: + Cache::LRU 0.04 + requirements: + ExtUtils::MakeMaker 6.42 + Test::More 0.88 + Test::Requires 0 + perl 5.008001 + Call-Context-0.05 + pathname: F/FE/FELIPE/Call-Context-0.05.tar.gz + provides: + Call::Context 0.05 + Call::Context::X 0.05 + requirements: + ExtUtils::MakeMaker 0 + Canary-Stability-2013 + pathname: M/ML/MLEHMANN/Canary-Stability-2013.tar.gz + provides: + Canary::Stability 2013 + requirements: + ExtUtils::MakeMaker 0 + Capture-Tiny-0.50 + pathname: D/DA/DAGOLDEN/Capture-Tiny-0.50.tar.gz + provides: + Capture::Tiny 0.50 + requirements: + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 6.17 + File::Spec 0 + File::Temp 0 + IO::Handle 0 + Scalar::Util 0 + perl 5.006 + strict 0 + warnings 0 + Carp-Assert-0.22 + pathname: Y/YV/YVES/Carp-Assert-0.22.tar.gz + provides: + Carp::Assert 0.22 + requirements: + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 0 + perl 5.006 + strict 0 + vars 0 + warnings 0 + Carp-Clan-6.08 + pathname: E/ET/ETHER/Carp-Clan-6.08.tar.gz + provides: + Carp::Clan 6.08 + requirements: + ExtUtils::MakeMaker 0 + overload 0 + perl 5.006 + strict 0 + Catalyst-Action-REST-1.21 + pathname: J/JJ/JJNAPIORK/Catalyst-Action-REST-1.21.tar.gz + provides: + Catalyst::Action::Deserialize 1.21 + Catalyst::Action::Deserialize::Callback 1.21 + Catalyst::Action::Deserialize::JSON 1.21 + Catalyst::Action::Deserialize::JSON::XS 1.21 + Catalyst::Action::Deserialize::View 1.21 + Catalyst::Action::Deserialize::XML::Simple 1.21 + Catalyst::Action::Deserialize::YAML 1.21 + Catalyst::Action::DeserializeMultiPart 1.21 + Catalyst::Action::REST 1.21 + Catalyst::Action::REST::ForBrowsers 1.21 + Catalyst::Action::Serialize 1.21 + Catalyst::Action::Serialize::Callback 1.21 + Catalyst::Action::Serialize::JSON 1.21 + Catalyst::Action::Serialize::JSON::XS 1.21 + Catalyst::Action::Serialize::JSONP 1.21 + Catalyst::Action::Serialize::View 1.21 + Catalyst::Action::Serialize::XML::Simple 1.21 + Catalyst::Action::Serialize::YAML 1.21 + Catalyst::Action::Serialize::YAML::HTML 1.21 + Catalyst::Action::SerializeBase 1.21 + Catalyst::Controller::REST 1.21 + Catalyst::Request::REST 1.21 + Catalyst::Request::REST::ForBrowsers 1.21 + Catalyst::TraitFor::Request::REST 1.21 + Catalyst::TraitFor::Request::REST::ForBrowsers 1.21 + requirements: + Catalyst::Runtime 5.80030 + Class::Inspector 1.13 + ExtUtils::MakeMaker 0 + JSON::MaybeXS 0 + MRO::Compat 0.10 + Module::Pluggable::Object 0 + Moose 1.03 + Params::Validate 0.76 + URI::Find 0 + namespace::autoclean 0 + Catalyst-Action-RenderView-0.17 + pathname: H/HA/HAARG/Catalyst-Action-RenderView-0.17.tar.gz + provides: + Catalyst::Action::RenderView 0.17 + requirements: + Catalyst::Runtime 5.80030 + Data::Visitor 0.24 + ExtUtils::MakeMaker 0 + MRO::Compat 0 + perl 5.008005 + Catalyst-Plugin-Authentication-0.10024 + pathname: J/JJ/JJNAPIORK/Catalyst-Plugin-Authentication-0.10024.tar.gz + provides: + Catalyst::Authentication::Credential::NoPassword undef + Catalyst::Authentication::Credential::Password undef + Catalyst::Authentication::Credential::Remote undef + Catalyst::Authentication::Realm undef + Catalyst::Authentication::Realm::Compatibility undef + Catalyst::Authentication::Realm::Progressive undef + Catalyst::Authentication::Store::Minimal undef + Catalyst::Authentication::Store::Null undef + Catalyst::Authentication::User undef + Catalyst::Authentication::User::Hash undef + Catalyst::Plugin::Authentication 0.10024 + Catalyst::Plugin::Authentication::Credential::Password undef + Catalyst::Plugin::Authentication::Store::Minimal undef + Catalyst::Plugin::Authentication::User undef + Catalyst::Plugin::Authentication::User::Hash undef + requirements: + Catalyst::Runtime 0 + ExtUtils::MakeMaker 0 + MRO::Compat 0 + Moose 0 + MooseX::Emulate::Class::Accessor::Fast 0 + String::RewritePrefix 0 + Try::Tiny 0 + namespace::autoclean 0 + Catalyst-Plugin-ConfigLoader-0.35 + pathname: H/HA/HAARG/Catalyst-Plugin-ConfigLoader-0.35.tar.gz + provides: + Catalyst::Plugin::ConfigLoader 0.35 + requirements: + Catalyst::Runtime 5.7008 + Config::Any 0.20 + Data::Visitor 0.24 + ExtUtils::MakeMaker 0 + MRO::Compat 0.09 + Catalyst-Plugin-Session-0.43 + pathname: H/HA/HAARG/Catalyst-Plugin-Session-0.43.tar.gz + provides: + Catalyst::Plugin::Session 0.43 + Catalyst::Plugin::Session::State 0.43 + Catalyst::Plugin::Session::Store 0.43 + Catalyst::Plugin::Session::Store::Dummy 0.43 + Catalyst::Plugin::Session::Test::Store 0.43 + requirements: + Catalyst::Runtime 5.71001 + Digest 0 + ExtUtils::MakeMaker 0 + File::Spec 0 + File::Temp 0 + HTML::Entities 0 + List::Util 0 + MRO::Compat 0 + Moose 0.76 + MooseX::Emulate::Class::Accessor::Fast 0.00801 + Object::Signature 0 + Test::More 0.88 + namespace::clean 0.10 + perl 5.008 + Catalyst-Plugin-Session-State-Cookie-0.18 + pathname: H/HA/HAARG/Catalyst-Plugin-Session-State-Cookie-0.18.tar.gz + provides: + Catalyst::Plugin::Session::State::Cookie 0.18 + requirements: + Catalyst 5.80005 + Catalyst::Plugin::Session 0.27 + ExtUtils::MakeMaker 0 + MRO::Compat 0 + Moose 0 + namespace::autoclean 0 + Catalyst-Plugin-Static-Simple-0.37 + pathname: I/IL/ILMARI/Catalyst-Plugin-Static-Simple-0.37.tar.gz + provides: + Catalyst::Plugin::Static::Simple 0.37 + requirements: + Catalyst::Runtime 5.80008 + ExtUtils::MakeMaker 0 + MIME::Types 2.03 + Moose 0 + namespace::autoclean 0 + Catalyst-Runtime-5.90132 + pathname: J/JJ/JJNAPIORK/Catalyst-Runtime-5.90132.tar.gz + provides: + Catalyst 5.90132 + Catalyst::Action undef + Catalyst::ActionChain undef + Catalyst::ActionContainer undef + Catalyst::ActionRole::ConsumesContent undef + Catalyst::ActionRole::HTTPMethods undef + Catalyst::ActionRole::QueryMatching undef + Catalyst::ActionRole::Scheme undef + Catalyst::Base undef + Catalyst::ClassData undef + Catalyst::Component undef + Catalyst::Component::ApplicationAttribute undef + Catalyst::Component::ContextClosure undef + Catalyst::Controller undef + Catalyst::DispatchType undef + Catalyst::DispatchType::Chained undef + Catalyst::DispatchType::Default undef + Catalyst::DispatchType::Index undef + Catalyst::DispatchType::Path undef + Catalyst::Dispatcher undef + Catalyst::Engine undef + Catalyst::EngineLoader undef + Catalyst::Exception undef + Catalyst::Exception::Base undef + Catalyst::Exception::Basic undef + Catalyst::Exception::Detach undef + Catalyst::Exception::Go undef + Catalyst::Exception::Interface undef + Catalyst::Log undef + Catalyst::Middleware::Stash undef + Catalyst::Model undef + Catalyst::Plugin::Unicode::Encoding 5.90132 + Catalyst::Request undef + Catalyst::Request::PartData undef + Catalyst::Request::Upload undef + Catalyst::Response undef + Catalyst::Response::Writer undef + Catalyst::Runtime 5.90132 + Catalyst::Script::CGI undef + Catalyst::Script::Create undef + Catalyst::Script::FastCGI undef + Catalyst::Script::Server undef + Catalyst::Script::Test undef + Catalyst::ScriptRole undef + Catalyst::ScriptRunner undef + Catalyst::Stats undef + Catalyst::Test undef + Catalyst::Utils undef + Catalyst::View undef + requirements: + CGI::Simple::Cookie 1.109 + CGI::Struct 0 + Carp 1.25 + Class::C3::Adopt::NEXT 0.07 + Class::Load 0.12 + Data::Dump 0 + Data::OptList 0 + Devel::InnerPackage 0 + Encode 2.49 + ExtUtils::MakeMaker 0 + HTML::Entities 0 + HTML::HeadParser 0 + HTTP::Body 1.22 + HTTP::Headers 1.64 + HTTP::Request 5.814 + HTTP::Response 5.813 + Hash::MultiValue 0 + JSON::MaybeXS 1.000000 + LWP 5.837 + List::Util 1.45 + MRO::Compat 0 + Module::Pluggable 4.7 + Moose 2.1400 + MooseX::Emulate::Class::Accessor::Fast 0.00903 + MooseX::Getopt 0.48 + MooseX::MethodAttributes::Role::AttrContainer::Inheritable 0.24 + Path::Class 0.09 + PerlIO::utf8_strict 0 + Plack 0.9991 + Plack::Middleware::Conditional 0 + Plack::Middleware::ContentLength 0 + Plack::Middleware::FixMissingBodyInRedirect 0.09 + Plack::Middleware::HTTPExceptions 0 + Plack::Middleware::Head 0 + Plack::Middleware::IIS6ScriptNameFix 0 + Plack::Middleware::IIS7KeepAliveFix 0 + Plack::Middleware::LighttpdScriptNameFix 0 + Plack::Middleware::MethodOverride 0.12 + Plack::Middleware::RemoveRedundantBody 0.03 + Plack::Middleware::ReverseProxy 0.04 + Plack::Request::Upload 0 + Plack::Test::ExternalServer 0 + Safe::Isa 0 + Scalar::Util 0 + Socket 1.96 + Stream::Buffered 0 + String::RewritePrefix 0.004 + Sub::Exporter 0 + Task::Weaken 0 + Text::Balanced 0 + Text::SimpleTable 0.03 + Time::HiRes 0 + Tree::Simple 1.15 + Tree::Simple::Visitor::FindByUID 0 + Try::Tiny 0.17 + URI 1.65 + URI::ws 0.03 + namespace::clean 0.23 + perl 5.008003 + Catalyst-View-JSON-0.37 + pathname: H/HA/HAARG/Catalyst-View-JSON-0.37.tar.gz + provides: + Catalyst::Helper::View::JSON undef + Catalyst::View::JSON 0.37 + requirements: + Catalyst 5.60 + ExtUtils::MakeMaker 0 + JSON::MaybeXS 1.003000 + MRO::Compat 0 + CatalystX-Fastly-Role-Response-0.07 + pathname: H/HA/HAARG/CatalystX-Fastly-Role-Response-0.07.tar.gz + provides: + CatalystX::Fastly::Role::Response 0.07 + requirements: + ExtUtils::MakeMaker 0 + Moose::Role 2.2200 + perl 5.008005 + Class-Accessor-0.51 + pathname: K/KA/KASEI/Class-Accessor-0.51.tar.gz + provides: + Class::Accessor 0.51 + Class::Accessor::Fast 0.51 + Class::Accessor::Faster 0.51 + requirements: + ExtUtils::MakeMaker 0 + base 1.01 + Class-C3-Adopt-NEXT-0.14 + pathname: E/ET/ETHER/Class-C3-Adopt-NEXT-0.14.tar.gz + provides: + Class::C3::Adopt::NEXT 0.14 + requirements: + List::Util 1.33 + MRO::Compat 0 + Module::Build::Tiny 0.039 + NEXT 0 + perl 5.006 + strict 0 + warnings 0 + warnings::register 0 + Class-Data-Inheritable-0.10 + pathname: R/RS/RSHERER/Class-Data-Inheritable-0.10.tar.gz + provides: + Class::Data::Inheritable 0.10 + requirements: + ExtUtils::MakeMaker 0 + Class-Inspector-1.36 + pathname: P/PL/PLICEASE/Class-Inspector-1.36.tar.gz + provides: + Class::Inspector 1.36 + Class::Inspector::Functions 1.36 + requirements: + ExtUtils::MakeMaker 0 + File::Spec 0.80 + base 0 + perl 5.008 + Class-Load-0.25 + pathname: E/ET/ETHER/Class-Load-0.25.tar.gz + provides: + Class::Load 0.25 + Class::Load::PP 0.25 + requirements: + Carp 0 + Data::OptList 0.110 + Exporter 0 + ExtUtils::MakeMaker 0 + Module::Implementation 0.04 + Module::Runtime 0.012 + Package::Stash 0.14 + Scalar::Util 0 + Try::Tiny 0 + base 0 + perl 5.006 + strict 0 + warnings 0 + Class-Load-XS-0.10 + pathname: E/ET/ETHER/Class-Load-XS-0.10.tar.gz + provides: + Class::Load::XS 0.10 + requirements: + Class::Load 0.20 + ExtUtils::MakeMaker 0 + XSLoader 0 + perl 5.006 + strict 0 + warnings 0 + Class-Method-Modifiers-2.15 + pathname: E/ET/ETHER/Class-Method-Modifiers-2.15.tar.gz + provides: + Class::Method::Modifiers 2.15 + requirements: + B 0 + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 0 + base 0 + perl 5.006 + strict 0 + warnings 0 + Class-Singleton-1.6 + pathname: S/SH/SHAY/Class-Singleton-1.6.tar.gz + provides: + Class::Singleton 1.6 + requirements: + ExtUtils::MakeMaker 6.64 + perl 5.008001 + strict 0 + warnings 0 + Class-Tiny-1.008 + pathname: D/DA/DAGOLDEN/Class-Tiny-1.008.tar.gz + provides: + Class::Tiny 1.008 + Class::Tiny::Object 1.008 + requirements: + Carp 0 + ExtUtils::MakeMaker 6.17 + perl 5.006 + strict 0 + warnings 0 + Class-Tiny-Chained-0.004 + pathname: D/DB/DBOOK/Class-Tiny-Chained-0.004.tar.gz + provides: + Class::Tiny::Chained 0.004 + requirements: + Class::Tiny 1.003 + ExtUtils::MakeMaker 0 + perl 5.006 + Class-XSAccessor-1.19 + pathname: S/SM/SMUELLER/Class-XSAccessor-1.19.tar.gz + provides: + Class::XSAccessor 1.19 + Class::XSAccessor::Array 1.19 + requirements: + ExtUtils::MakeMaker 0 + Test::More 0 + Time::HiRes 0 + XSLoader 0 + perl 5.008 + Clone-0.47 + pathname: A/AT/ATOOMIC/Clone-0.47.tar.gz + provides: + Clone 0.47 + requirements: + ExtUtils::MakeMaker 0 + Clone-Choose-0.010 + pathname: H/HE/HERMES/Clone-Choose-0.010.tar.gz + provides: + Clone::Choose 0.010 + requirements: + ExtUtils::MakeMaker 0 + Storable 0 + perl 5.008001 + Code-TidyAll-0.84 + pathname: D/DR/DROLSKY/Code-TidyAll-0.84.tar.gz + provides: + Code::TidyAll 0.84 + Code::TidyAll::Cache 0.84 + Code::TidyAll::CacheModel 0.84 + Code::TidyAll::CacheModel::Shared 0.84 + Code::TidyAll::Config::INI::Reader 0.84 + Code::TidyAll::Git::Precommit 0.84 + Code::TidyAll::Git::Prereceive 0.84 + Code::TidyAll::Git::Util 0.84 + Code::TidyAll::Plugin 0.84 + Code::TidyAll::Plugin::CSSUnminifier 0.84 + Code::TidyAll::Plugin::GenericTransformer 0.84 + Code::TidyAll::Plugin::GenericValidator 0.84 + Code::TidyAll::Plugin::JSBeautify 0.84 + Code::TidyAll::Plugin::JSHint 0.84 + Code::TidyAll::Plugin::JSLint 0.84 + Code::TidyAll::Plugin::JSON 0.84 + Code::TidyAll::Plugin::MasonTidy 0.84 + Code::TidyAll::Plugin::PHPCodeSniffer 0.84 + Code::TidyAll::Plugin::PerlCritic 0.84 + Code::TidyAll::Plugin::PerlTidy 0.84 + Code::TidyAll::Plugin::PerlTidySweet 0.84 + Code::TidyAll::Plugin::PodChecker 0.84 + Code::TidyAll::Plugin::PodSpell 0.84 + Code::TidyAll::Plugin::PodTidy 0.84 + Code::TidyAll::Plugin::SortLines 0.84 + Code::TidyAll::Result 0.84 + Code::TidyAll::Role::GenericExecutable 0.84 + Code::TidyAll::Role::HasIgnore 0.84 + Code::TidyAll::Role::RunsCommand 0.84 + Code::TidyAll::Role::Tempdir 0.84 + Code::TidyAll::SVN::Precommit 0.84 + Code::TidyAll::SVN::Util 0.84 + Code::TidyAll::Util::Zglob 0.84 + Code::TidyAll::Zglob 0.84 + Test::Code::TidyAll 0.84 + requirements: + Capture::Tiny 0 + Config::INI::Reader 0 + Cwd 0 + Data::Dumper 0 + Date::Format 0 + Digest::SHA 0 + Exporter 0 + ExtUtils::MakeMaker 0 + File::Basename 0 + File::Find 0 + File::Spec 0 + File::Which 0 + File::pushd 0 + Getopt::Long 0 + IPC::Run3 0 + IPC::System::Simple 0 + List::Compare 0 + List::SomeUtils 0 + Log::Any 0 + Module::Runtime 0 + Moo 2.000000 + Moo::Role 0 + Path::Tiny 0.098 + Scalar::Util 0 + Scope::Guard 0 + Specio 0.40 + Specio::Declare 0 + Specio::Library::Builtins 0 + Specio::Library::Numeric 0 + Specio::Library::Path::Tiny 0.04 + Specio::Library::String 0 + Test::Builder 0 + Text::Diff 1.44 + Text::Diff::Table 0 + Text::ParseWords 0 + Time::Duration::Parse 0 + Try::Tiny 0 + base 0 + constant 0 + perl 5.008008 + strict 0 + warnings 0 + Code-TidyAll-Plugin-UniqueLines-0.000003 + pathname: O/OA/OALDERS/Code-TidyAll-Plugin-UniqueLines-0.000003.tar.gz + provides: + Code::TidyAll::Plugin::UniqueLines 0.000003 + requirements: + Code::TidyAll::Plugin 0 + ExtUtils::MakeMaker 0 + List::Util 1.45 + Moo 0 + perl 5.006 + strict 0 + warnings 0 + Compress-Bzip2-2.28 + pathname: R/RU/RURBAN/Compress-Bzip2-2.28.tar.gz + provides: + Compress::Bzip2 2.28 + requirements: + Carp 0 + Config 0 + ExtUtils::MakeMaker 0 + Fcntl 0 + File::Copy 0 + File::Spec 0 + Getopt::Std 0 + Test::More 0 + constant 1.04 + Compress-Raw-Bzip2-2.213 + pathname: P/PM/PMQS/Compress-Raw-Bzip2-2.213.tar.gz + provides: + Compress::Raw::Bzip2 2.213 + requirements: + ExtUtils::MakeMaker 0 + Compress-Raw-Zlib-2.213 + pathname: P/PM/PMQS/Compress-Raw-Zlib-2.213.tar.gz + provides: + Compress::Raw::Zlib 2.213 + requirements: + ExtUtils::MakeMaker 0 + Config-Any-0.33 + pathname: H/HA/HAARG/Config-Any-0.33.tar.gz + provides: + Config::Any 0.33 + Config::Any::Base undef + Config::Any::General undef + Config::Any::INI undef + Config::Any::JSON undef + Config::Any::Perl undef + Config::Any::XML undef + Config::Any::YAML undef + requirements: + Module::Pluggable::Object 3.6 + Config-General-2.67 + pathname: T/TL/TLINDEN/Config-General-2.67.tar.gz + provides: + Config::General 2.67 + Config::General::Extended 2.07 + Config::General::Interpolated 2.16 + requirements: + ExtUtils::MakeMaker 0 + File::Glob 0 + File::Spec::Functions 0 + FileHandle 0 + IO::File 0 + Config-INI-0.029 + pathname: R/RJ/RJBS/Config-INI-0.029.tar.gz + provides: + Config::INI 0.029 + Config::INI::Reader 0.029 + Config::INI::Writer 0.029 + requirements: + Carp 0 + ExtUtils::MakeMaker 6.78 + Mixin::Linewise::Readers 0.110 + Mixin::Linewise::Writers 0 + perl 5.012 + warnings 0 + Config-Tiny-2.30 + pathname: R/RS/RSAVAGE/Config-Tiny-2.30.tgz + provides: + Config::Tiny 2.30 + requirements: + ExtUtils::MakeMaker 0 + File::Spec 3.30 + File::Temp 0.22 + Test::More 0.47 + perl 5.008001 + strict 0 + utf8 0 + Config-ZOMG-1.000000 + pathname: F/FR/FREW/Config-ZOMG-1.000000.tar.gz + provides: + Config::ZOMG 1.000000 + Config::ZOMG::Source::Loader 1.000000 + requirements: + Clone 0 + Config::Any 0 + ExtUtils::MakeMaker 6.30 + Hash::Merge::Simple 0 + List::Util 0 + Moo 0 + Const-Fast-0.014 + pathname: L/LE/LEONT/Const-Fast-0.014.tar.gz + provides: + Const::Fast 0.014 + requirements: + Carp 0 + Module::Build::Tiny 0.021 + Scalar::Util 0 + Storable 0 + Sub::Exporter::Progressive 0.001007 + perl 5.008 + strict 0 + warnings 0 + Cookie-Baker-0.12 + pathname: K/KA/KAZEBURO/Cookie-Baker-0.12.tar.gz + provides: + Cookie::Baker 0.12 + requirements: + Exporter 0 + Module::Build::Tiny 0.035 + URI::Escape 0 + perl 5.008001 + Cpanel-JSON-XS-4.39 + pathname: R/RU/RURBAN/Cpanel-JSON-XS-4.39.tar.gz + provides: + Cpanel::JSON::XS 4.39 + Cpanel::JSON::XS::Type undef + requirements: + Carp 0 + Config 0 + Encode 1.9801 + Exporter 0 + ExtUtils::MakeMaker 0 + Pod::Text 2.08 + XSLoader 0 + overload 0 + strict 0 + warnings 0 + Crypt-SysRandom-0.007 + pathname: L/LE/LEONT/Crypt-SysRandom-0.007.tar.gz + provides: + Crypt::SysRandom 0.007 + requirements: + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 0 + perl 5.006 + strict 0 + warnings 0 + Crypt-URandom-0.54 + pathname: D/DD/DDICK/Crypt-URandom-0.54.tar.gz + provides: + Crypt::URandom 0.54 + requirements: + Carp 1.26 + English 0 + Exporter 0 + ExtUtils::MakeMaker 0 + FileHandle 0 + constant 0 + perl 5.006 + DBD-Pg-3.18.0 + pathname: T/TU/TURNSTEP/DBD-Pg-3.18.0.tar.gz + provides: + Bundle::DBD::Pg v3.18.0 + DBD::Pg v3.18.0 + requirements: + DBI 1.614 + ExtUtils::MakeMaker 6.58 + File::Temp 0 + Test::More 0.88 + Time::HiRes 0 + version 0 + DBD-SQLite-1.76 + pathname: I/IS/ISHIGAKI/DBD-SQLite-1.76.tar.gz + provides: + DBD::SQLite 1.76 + DBD::SQLite::Constants undef + DBD::SQLite::GetInfo undef + DBD::SQLite::VirtualTable 1.76 + DBD::SQLite::VirtualTable::Cursor 1.76 + DBD::SQLite::VirtualTable::FileContent undef + DBD::SQLite::VirtualTable::FileContent::Cursor undef + DBD::SQLite::VirtualTable::PerlData undef + DBD::SQLite::VirtualTable::PerlData::Cursor undef + requirements: + DBI 1.57 + ExtUtils::MakeMaker 0 + File::Spec 0.82 + Test::More 0.88 + Tie::Hash 0 + perl 5.006 + DBI-1.647 + pathname: H/HM/HMBRAND/DBI-1.647.tgz + provides: + Bundle::DBI 12.008696 + DBD::DBM 0.08 + DBD::DBM::Statement 0.08 + DBD::DBM::Table 0.08 + DBD::DBM::db 0.08 + DBD::DBM::dr 0.08 + DBD::DBM::st 0.08 + DBD::ExampleP 12.014311 + DBD::ExampleP::db 12.014311 + DBD::ExampleP::dr 12.014311 + DBD::ExampleP::st 12.014311 + DBD::File 0.44 + DBD::File::DataSource::File 0.44 + DBD::File::DataSource::Stream 0.44 + DBD::File::Statement 0.44 + DBD::File::Table 0.44 + DBD::File::TableSource::FileSystem 0.44 + DBD::File::db 0.44 + DBD::File::dr 0.44 + DBD::File::st 0.44 + DBD::Gofer 0.015327 + DBD::Gofer::Policy::Base 0.010088 + DBD::Gofer::Policy::classic 0.010088 + DBD::Gofer::Policy::pedantic 0.010088 + DBD::Gofer::Policy::rush 0.010088 + DBD::Gofer::Transport::Base 0.014121 + DBD::Gofer::Transport::corostream undef + DBD::Gofer::Transport::null 0.010088 + DBD::Gofer::Transport::pipeone 0.010088 + DBD::Gofer::Transport::stream 0.014599 + DBD::Gofer::db 0.015327 + DBD::Gofer::dr 0.015327 + DBD::Gofer::st 0.015327 + DBD::Mem 0.001 + DBD::Mem::DataSource 0.001 + DBD::Mem::Statement 0.001 + DBD::Mem::Table 0.001 + DBD::Mem::db 0.001 + DBD::Mem::dr 0.001 + DBD::Mem::st 0.001 + DBD::NullP 12.014715 + DBD::NullP::db 12.014715 + DBD::NullP::dr 12.014715 + DBD::NullP::st 12.014715 + DBD::Proxy 0.2004 + DBD::Proxy::RPC::PlClient 0.2004 + DBD::Proxy::db 0.2004 + DBD::Proxy::dr 0.2004 + DBD::Proxy::st 0.2004 + DBD::Sponge 12.010003 + DBD::Sponge::db 12.010003 + DBD::Sponge::dr 12.010003 + DBD::Sponge::st 12.010003 + DBDI 12.015129 + DBI 1.647 + DBI::Const::GetInfo::ANSI 2.008697 + DBI::Const::GetInfo::ODBC 2.011374 + DBI::Const::GetInfoReturn 2.008697 + DBI::Const::GetInfoType 2.008697 + DBI::DBD 12.015129 + DBI::DBD::Metadata 2.014214 + DBI::DBD::SqlEngine 0.06 + DBI::DBD::SqlEngine::DataSource 0.06 + DBI::DBD::SqlEngine::Statement 0.06 + DBI::DBD::SqlEngine::Table 0.06 + DBI::DBD::SqlEngine::TableSource 0.06 + DBI::DBD::SqlEngine::TieMeta 0.06 + DBI::DBD::SqlEngine::TieTables 0.06 + DBI::DBD::SqlEngine::db 0.06 + DBI::DBD::SqlEngine::dr 0.06 + DBI::DBD::SqlEngine::st 0.06 + DBI::Gofer::Execute 0.014283 + DBI::Gofer::Request 0.012537 + DBI::Gofer::Response 0.011566 + DBI::Gofer::Serializer::Base 0.009950 + DBI::Gofer::Serializer::DataDumper 0.009950 + DBI::Gofer::Serializer::Storable 0.015586 + DBI::Gofer::Transport::Base 0.012537 + DBI::Gofer::Transport::pipeone 0.012537 + DBI::Gofer::Transport::stream 0.012537 + DBI::Profile 2.015065 + DBI::ProfileData 2.010008 + DBI::ProfileDumper 2.015325 + DBI::ProfileDumper::Apache 2.014121 + DBI::ProfileSubs 0.009396 + DBI::ProxyServer 0.3005 + DBI::ProxyServer::db 0.3005 + DBI::ProxyServer::dr 0.3005 + DBI::ProxyServer::st 0.3005 + DBI::SQL::Nano 1.015544 + DBI::SQL::Nano::Statement_ 1.015544 + DBI::SQL::Nano::Table_ 1.015544 + DBI::Util::CacheMemory 0.010315 + DBI::Util::_accessor 0.009479 + DBI::common 1.647 + requirements: + ExtUtils::MakeMaker 6.48 + Test::Simple 0.90 + perl 5.008001 + Data-Compare-1.29 + pathname: D/DC/DCANTRELL/Data-Compare-1.29.tar.gz + provides: + Data::Compare 1.29 + Data::Compare::Plugins::Scalar::Properties 1.25 + requirements: + Clone 0.43 + ExtUtils::MakeMaker 6.48 + File::Find::Rule 0.1 + Scalar::Util 0 + Test::More 0.88 + perl 5.006 + Data-Dump-1.25 + pathname: G/GA/GARU/Data-Dump-1.25.tar.gz + provides: + Data::Dump 1.25 + Data::Dump::FilterContext undef + Data::Dump::Filtered undef + Data::Dump::Trace 0.02 + Data::Dump::Trace::Call 0.02 + Data::Dump::Trace::Wrapper 0.02 + requirements: + ExtUtils::MakeMaker 0 + Symbol 0 + Test 0 + perl 5.006 + Data-Dumper-Concise-2.023 + pathname: E/ET/ETHER/Data-Dumper-Concise-2.023.tar.gz + provides: + Data::Dumper::Concise 2.023 + Data::Dumper::Concise::Sugar 2.023 + Devel::Dwarn undef + requirements: + Data::Dumper 0 + Exporter 0 + ExtUtils::MakeMaker 0 + perl 5.006 + Data-OptList-0.114 + pathname: R/RJ/RJBS/Data-OptList-0.114.tar.gz + provides: + Data::OptList 0.114 + requirements: + ExtUtils::MakeMaker 6.78 + List::Util 0 + Params::Util 0 + Sub::Install 0.921 + perl 5.012 + strict 0 + warnings 0 + Data-Section-0.200008 + pathname: R/RJ/RJBS/Data-Section-0.200008.tar.gz + provides: + Data::Section 0.200008 + requirements: + Encode 0 + ExtUtils::MakeMaker 6.78 + MRO::Compat 0.09 + Sub::Exporter 0.979 + perl 5.012 + strict 0 + warnings 0 + Data-UUID-1.227 + pathname: G/GT/GTERMARS/Data-UUID-1.227.tar.gz + provides: + Data::UUID 1.227 + requirements: + Digest::MD5 0 + ExtUtils::MakeMaker 0 + Data-Visitor-0.32 + pathname: E/ET/ETHER/Data-Visitor-0.32.tar.gz + provides: + Data::Visitor 0.32 + Data::Visitor::Callback 0.32 + requirements: + Carp 0 + ExtUtils::MakeMaker 0 + Moose 0.89 + Scalar::Util 0 + Symbol 0 + Tie::ToObject 0.01 + constant 0 + namespace::clean 0.19 + overload 0 + perl 5.006 + strict 0 + warnings 0 + DateTime-1.66 + pathname: D/DR/DROLSKY/DateTime-1.66.tar.gz + provides: + DateTime 1.66 + DateTime::Duration 1.66 + DateTime::Helpers 1.66 + DateTime::Infinite 1.66 + DateTime::Infinite::Future 1.66 + DateTime::Infinite::Past 1.66 + DateTime::LeapSecond 1.66 + DateTime::PP 1.66 + DateTime::PPExtra 1.66 + DateTime::Types 1.66 + requirements: + Carp 0 + DateTime::Locale 1.06 + DateTime::TimeZone 2.44 + Dist::CheckConflicts 0.02 + ExtUtils::MakeMaker 0 + POSIX 0 + Params::ValidationCompiler 0.26 + Scalar::Util 0 + Specio 0.50 + Specio::Declare 0 + Specio::Exporter 0 + Specio::Library::Builtins 0 + Specio::Library::Numeric 0 + Specio::Library::String 0 + Specio::Subs 0 + Try::Tiny 0 + XSLoader 0 + integer 0 + namespace::autoclean 0.19 + overload 0 + parent 0 + perl 5.008004 + strict 0 + warnings 0 + warnings::register 0 + DateTime-Format-Builder-0.83 + pathname: D/DR/DROLSKY/DateTime-Format-Builder-0.83.tar.gz + provides: + DateTime::Format::Builder 0.83 + DateTime::Format::Builder::Parser 0.83 + DateTime::Format::Builder::Parser::Dispatch 0.83 + DateTime::Format::Builder::Parser::Quick 0.83 + DateTime::Format::Builder::Parser::Regex 0.83 + DateTime::Format::Builder::Parser::Strptime 0.83 + DateTime::Format::Builder::Parser::generic 0.83 + requirements: + Carp 0 + DateTime 1.00 + DateTime::Format::Strptime 1.04 + ExtUtils::MakeMaker 0 + Params::Validate 0.72 + Scalar::Util 0 + parent 0 + strict 0 + warnings 0 + DateTime-Format-Epoch-0.16 + pathname: C/CH/CHORNY/DateTime-Format-Epoch-0.16.tar.gz + provides: + DateTime::Format::Epoch 0.16 + DateTime::Format::Epoch::ActiveDirectory 0.13 + DateTime::Format::Epoch::DotNet 0.13 + DateTime::Format::Epoch::JD 0.13 + DateTime::Format::Epoch::Lilian 0.13 + DateTime::Format::Epoch::MJD 0.13 + DateTime::Format::Epoch::MacOS 0.13 + DateTime::Format::Epoch::NTP 0.14 + DateTime::Format::Epoch::RJD 0.13 + DateTime::Format::Epoch::RataDie 0.13 + DateTime::Format::Epoch::TAI64 0.13 + DateTime::Format::Epoch::TJD 0.13 + DateTime::Format::Epoch::Unix 0.13 + requirements: + DateTime 0.31 + Math::BigInt 1.66 + Params::Validate 0 + Test::More 0 + perl 5.00503 + warnings 0 + DateTime-Format-ISO8601-0.17 + pathname: D/DR/DROLSKY/DateTime-Format-ISO8601-0.17.tar.gz + provides: + DateTime::Format::ISO8601 0.17 + DateTime::Format::ISO8601::Types 0.17 + requirements: + Carp 0 + DateTime 1.45 + DateTime::Format::Builder 0.77 + ExtUtils::MakeMaker 0 + Params::ValidationCompiler 0.26 + Specio 0.18 + Specio::Declare 0 + Specio::Exporter 0 + Specio::Library::Builtins 0 + namespace::autoclean 0 + parent 0 + strict 0 + warnings 0 + DateTime-Format-RFC3339-v1.10.0 + pathname: I/IK/IKEGAMI/DateTime-Format-RFC3339-v1.10.0.tar.gz + provides: + DateTime::Format::RFC3339 1.010000 + requirements: + DateTime 0 + ExtUtils::MakeMaker 0 + perl 5.01 + strict 0 + version 0 + warnings 0 + DateTime-Format-Strptime-1.79 + pathname: D/DR/DROLSKY/DateTime-Format-Strptime-1.79.tar.gz + provides: + DateTime::Format::Strptime 1.79 + DateTime::Format::Strptime::Types 1.79 + requirements: + Carp 0 + DateTime 1.00 + DateTime::Locale 1.30 + DateTime::Locale::Base 0 + DateTime::Locale::FromData 0 + DateTime::TimeZone 2.09 + Exporter 0 + ExtUtils::MakeMaker 0 + Params::ValidationCompiler 0 + Specio 0.33 + Specio::Declare 0 + Specio::Exporter 0 + Specio::Library::Builtins 0 + Specio::Library::String 0 + Try::Tiny 0 + constant 0 + parent 0 + strict 0 + warnings 0 + DateTime-Locale-1.45 + pathname: D/DR/DROLSKY/DateTime-Locale-1.45.tar.gz + provides: + DateTime::Locale 1.45 + DateTime::Locale::Base 1.45 + DateTime::Locale::Catalog 1.45 + DateTime::Locale::Data 1.45 + DateTime::Locale::FromData 1.45 + DateTime::Locale::Util 1.45 + requirements: + Carp 0 + Dist::CheckConflicts 0.02 + Exporter 0 + ExtUtils::MakeMaker 0 + File::ShareDir 0 + File::ShareDir::Install 0.06 + File::Spec 0 + List::Util 1.45 + Params::ValidationCompiler 0.13 + Specio::Declare 0 + Specio::Library::String 0 + Storable 0 + namespace::autoclean 0.19 + perl 5.008004 + strict 0 + warnings 0 + DateTime-TimeZone-2.65 + pathname: D/DR/DROLSKY/DateTime-TimeZone-2.65.tar.gz + provides: + DateTime::TimeZone 2.65 + DateTime::TimeZone::Africa::Abidjan 2.65 + DateTime::TimeZone::Africa::Algiers 2.65 + DateTime::TimeZone::Africa::Bissau 2.65 + DateTime::TimeZone::Africa::Cairo 2.65 + DateTime::TimeZone::Africa::Casablanca 2.65 + DateTime::TimeZone::Africa::Ceuta 2.65 + DateTime::TimeZone::Africa::El_Aaiun 2.65 + DateTime::TimeZone::Africa::Johannesburg 2.65 + DateTime::TimeZone::Africa::Juba 2.65 + DateTime::TimeZone::Africa::Khartoum 2.65 + DateTime::TimeZone::Africa::Lagos 2.65 + DateTime::TimeZone::Africa::Maputo 2.65 + DateTime::TimeZone::Africa::Monrovia 2.65 + DateTime::TimeZone::Africa::Nairobi 2.65 + DateTime::TimeZone::Africa::Ndjamena 2.65 + DateTime::TimeZone::Africa::Sao_Tome 2.65 + DateTime::TimeZone::Africa::Tripoli 2.65 + DateTime::TimeZone::Africa::Tunis 2.65 + DateTime::TimeZone::Africa::Windhoek 2.65 + DateTime::TimeZone::America::Adak 2.65 + DateTime::TimeZone::America::Anchorage 2.65 + DateTime::TimeZone::America::Araguaina 2.65 + DateTime::TimeZone::America::Argentina::Buenos_Aires 2.65 + DateTime::TimeZone::America::Argentina::Catamarca 2.65 + DateTime::TimeZone::America::Argentina::Cordoba 2.65 + DateTime::TimeZone::America::Argentina::Jujuy 2.65 + DateTime::TimeZone::America::Argentina::La_Rioja 2.65 + DateTime::TimeZone::America::Argentina::Mendoza 2.65 + DateTime::TimeZone::America::Argentina::Rio_Gallegos 2.65 + DateTime::TimeZone::America::Argentina::Salta 2.65 + DateTime::TimeZone::America::Argentina::San_Juan 2.65 + DateTime::TimeZone::America::Argentina::San_Luis 2.65 + DateTime::TimeZone::America::Argentina::Tucuman 2.65 + DateTime::TimeZone::America::Argentina::Ushuaia 2.65 + DateTime::TimeZone::America::Asuncion 2.65 + DateTime::TimeZone::America::Bahia 2.65 + DateTime::TimeZone::America::Bahia_Banderas 2.65 + DateTime::TimeZone::America::Barbados 2.65 + DateTime::TimeZone::America::Belem 2.65 + DateTime::TimeZone::America::Belize 2.65 + DateTime::TimeZone::America::Boa_Vista 2.65 + DateTime::TimeZone::America::Bogota 2.65 + DateTime::TimeZone::America::Boise 2.65 + DateTime::TimeZone::America::Cambridge_Bay 2.65 + DateTime::TimeZone::America::Campo_Grande 2.65 + DateTime::TimeZone::America::Cancun 2.65 + DateTime::TimeZone::America::Caracas 2.65 + DateTime::TimeZone::America::Cayenne 2.65 + DateTime::TimeZone::America::Chicago 2.65 + DateTime::TimeZone::America::Chihuahua 2.65 + DateTime::TimeZone::America::Ciudad_Juarez 2.65 + DateTime::TimeZone::America::Costa_Rica 2.65 + DateTime::TimeZone::America::Coyhaique 2.65 + DateTime::TimeZone::America::Cuiaba 2.65 + DateTime::TimeZone::America::Danmarkshavn 2.65 + DateTime::TimeZone::America::Dawson 2.65 + DateTime::TimeZone::America::Dawson_Creek 2.65 + DateTime::TimeZone::America::Denver 2.65 + DateTime::TimeZone::America::Detroit 2.65 + DateTime::TimeZone::America::Edmonton 2.65 + DateTime::TimeZone::America::Eirunepe 2.65 + DateTime::TimeZone::America::El_Salvador 2.65 + DateTime::TimeZone::America::Fort_Nelson 2.65 + DateTime::TimeZone::America::Fortaleza 2.65 + DateTime::TimeZone::America::Glace_Bay 2.65 + DateTime::TimeZone::America::Goose_Bay 2.65 + DateTime::TimeZone::America::Grand_Turk 2.65 + DateTime::TimeZone::America::Guatemala 2.65 + DateTime::TimeZone::America::Guayaquil 2.65 + DateTime::TimeZone::America::Guyana 2.65 + DateTime::TimeZone::America::Halifax 2.65 + DateTime::TimeZone::America::Havana 2.65 + DateTime::TimeZone::America::Hermosillo 2.65 + DateTime::TimeZone::America::Indiana::Indianapolis 2.65 + DateTime::TimeZone::America::Indiana::Knox 2.65 + DateTime::TimeZone::America::Indiana::Marengo 2.65 + DateTime::TimeZone::America::Indiana::Petersburg 2.65 + DateTime::TimeZone::America::Indiana::Tell_City 2.65 + DateTime::TimeZone::America::Indiana::Vevay 2.65 + DateTime::TimeZone::America::Indiana::Vincennes 2.65 + DateTime::TimeZone::America::Indiana::Winamac 2.65 + DateTime::TimeZone::America::Inuvik 2.65 + DateTime::TimeZone::America::Iqaluit 2.65 + DateTime::TimeZone::America::Jamaica 2.65 + DateTime::TimeZone::America::Juneau 2.65 + DateTime::TimeZone::America::Kentucky::Louisville 2.65 + DateTime::TimeZone::America::Kentucky::Monticello 2.65 + DateTime::TimeZone::America::La_Paz 2.65 + DateTime::TimeZone::America::Lima 2.65 + DateTime::TimeZone::America::Los_Angeles 2.65 + DateTime::TimeZone::America::Maceio 2.65 + DateTime::TimeZone::America::Managua 2.65 + DateTime::TimeZone::America::Manaus 2.65 + DateTime::TimeZone::America::Martinique 2.65 + DateTime::TimeZone::America::Matamoros 2.65 + DateTime::TimeZone::America::Mazatlan 2.65 + DateTime::TimeZone::America::Menominee 2.65 + DateTime::TimeZone::America::Merida 2.65 + DateTime::TimeZone::America::Metlakatla 2.65 + DateTime::TimeZone::America::Mexico_City 2.65 + DateTime::TimeZone::America::Miquelon 2.65 + DateTime::TimeZone::America::Moncton 2.65 + DateTime::TimeZone::America::Monterrey 2.65 + DateTime::TimeZone::America::Montevideo 2.65 + DateTime::TimeZone::America::New_York 2.65 + DateTime::TimeZone::America::Nome 2.65 + DateTime::TimeZone::America::Noronha 2.65 + DateTime::TimeZone::America::North_Dakota::Beulah 2.65 + DateTime::TimeZone::America::North_Dakota::Center 2.65 + DateTime::TimeZone::America::North_Dakota::New_Salem 2.65 + DateTime::TimeZone::America::Nuuk 2.65 + DateTime::TimeZone::America::Ojinaga 2.65 + DateTime::TimeZone::America::Panama 2.65 + DateTime::TimeZone::America::Paramaribo 2.65 + DateTime::TimeZone::America::Phoenix 2.65 + DateTime::TimeZone::America::Port_au_Prince 2.65 + DateTime::TimeZone::America::Porto_Velho 2.65 + DateTime::TimeZone::America::Puerto_Rico 2.65 + DateTime::TimeZone::America::Punta_Arenas 2.65 + DateTime::TimeZone::America::Rankin_Inlet 2.65 + DateTime::TimeZone::America::Recife 2.65 + DateTime::TimeZone::America::Regina 2.65 + DateTime::TimeZone::America::Resolute 2.65 + DateTime::TimeZone::America::Rio_Branco 2.65 + DateTime::TimeZone::America::Santarem 2.65 + DateTime::TimeZone::America::Santiago 2.65 + DateTime::TimeZone::America::Santo_Domingo 2.65 + DateTime::TimeZone::America::Sao_Paulo 2.65 + DateTime::TimeZone::America::Scoresbysund 2.65 + DateTime::TimeZone::America::Sitka 2.65 + DateTime::TimeZone::America::St_Johns 2.65 + DateTime::TimeZone::America::Swift_Current 2.65 + DateTime::TimeZone::America::Tegucigalpa 2.65 + DateTime::TimeZone::America::Thule 2.65 + DateTime::TimeZone::America::Tijuana 2.65 + DateTime::TimeZone::America::Toronto 2.65 + DateTime::TimeZone::America::Vancouver 2.65 + DateTime::TimeZone::America::Whitehorse 2.65 + DateTime::TimeZone::America::Winnipeg 2.65 + DateTime::TimeZone::America::Yakutat 2.65 + DateTime::TimeZone::Antarctica::Casey 2.65 + DateTime::TimeZone::Antarctica::Davis 2.65 + DateTime::TimeZone::Antarctica::Macquarie 2.65 + DateTime::TimeZone::Antarctica::Mawson 2.65 + DateTime::TimeZone::Antarctica::Palmer 2.65 + DateTime::TimeZone::Antarctica::Rothera 2.65 + DateTime::TimeZone::Antarctica::Troll 2.65 + DateTime::TimeZone::Antarctica::Vostok 2.65 + DateTime::TimeZone::Asia::Almaty 2.65 + DateTime::TimeZone::Asia::Amman 2.65 + DateTime::TimeZone::Asia::Anadyr 2.65 + DateTime::TimeZone::Asia::Aqtau 2.65 + DateTime::TimeZone::Asia::Aqtobe 2.65 + DateTime::TimeZone::Asia::Ashgabat 2.65 + DateTime::TimeZone::Asia::Atyrau 2.65 + DateTime::TimeZone::Asia::Baghdad 2.65 + DateTime::TimeZone::Asia::Baku 2.65 + DateTime::TimeZone::Asia::Bangkok 2.65 + DateTime::TimeZone::Asia::Barnaul 2.65 + DateTime::TimeZone::Asia::Beirut 2.65 + DateTime::TimeZone::Asia::Bishkek 2.65 + DateTime::TimeZone::Asia::Chita 2.65 + DateTime::TimeZone::Asia::Colombo 2.65 + DateTime::TimeZone::Asia::Damascus 2.65 + DateTime::TimeZone::Asia::Dhaka 2.65 + DateTime::TimeZone::Asia::Dili 2.65 + DateTime::TimeZone::Asia::Dubai 2.65 + DateTime::TimeZone::Asia::Dushanbe 2.65 + DateTime::TimeZone::Asia::Famagusta 2.65 + DateTime::TimeZone::Asia::Gaza 2.65 + DateTime::TimeZone::Asia::Hebron 2.65 + DateTime::TimeZone::Asia::Ho_Chi_Minh 2.65 + DateTime::TimeZone::Asia::Hong_Kong 2.65 + DateTime::TimeZone::Asia::Hovd 2.65 + DateTime::TimeZone::Asia::Irkutsk 2.65 + DateTime::TimeZone::Asia::Jakarta 2.65 + DateTime::TimeZone::Asia::Jayapura 2.65 + DateTime::TimeZone::Asia::Jerusalem 2.65 + DateTime::TimeZone::Asia::Kabul 2.65 + DateTime::TimeZone::Asia::Kamchatka 2.65 + DateTime::TimeZone::Asia::Karachi 2.65 + DateTime::TimeZone::Asia::Kathmandu 2.65 + DateTime::TimeZone::Asia::Khandyga 2.65 + DateTime::TimeZone::Asia::Kolkata 2.65 + DateTime::TimeZone::Asia::Krasnoyarsk 2.65 + DateTime::TimeZone::Asia::Kuching 2.65 + DateTime::TimeZone::Asia::Macau 2.65 + DateTime::TimeZone::Asia::Magadan 2.65 + DateTime::TimeZone::Asia::Makassar 2.65 + DateTime::TimeZone::Asia::Manila 2.65 + DateTime::TimeZone::Asia::Nicosia 2.65 + DateTime::TimeZone::Asia::Novokuznetsk 2.65 + DateTime::TimeZone::Asia::Novosibirsk 2.65 + DateTime::TimeZone::Asia::Omsk 2.65 + DateTime::TimeZone::Asia::Oral 2.65 + DateTime::TimeZone::Asia::Pontianak 2.65 + DateTime::TimeZone::Asia::Pyongyang 2.65 + DateTime::TimeZone::Asia::Qatar 2.65 + DateTime::TimeZone::Asia::Qostanay 2.65 + DateTime::TimeZone::Asia::Qyzylorda 2.65 + DateTime::TimeZone::Asia::Riyadh 2.65 + DateTime::TimeZone::Asia::Sakhalin 2.65 + DateTime::TimeZone::Asia::Samarkand 2.65 + DateTime::TimeZone::Asia::Seoul 2.65 + DateTime::TimeZone::Asia::Shanghai 2.65 + DateTime::TimeZone::Asia::Singapore 2.65 + DateTime::TimeZone::Asia::Srednekolymsk 2.65 + DateTime::TimeZone::Asia::Taipei 2.65 + DateTime::TimeZone::Asia::Tashkent 2.65 + DateTime::TimeZone::Asia::Tbilisi 2.65 + DateTime::TimeZone::Asia::Tehran 2.65 + DateTime::TimeZone::Asia::Thimphu 2.65 + DateTime::TimeZone::Asia::Tokyo 2.65 + DateTime::TimeZone::Asia::Tomsk 2.65 + DateTime::TimeZone::Asia::Ulaanbaatar 2.65 + DateTime::TimeZone::Asia::Urumqi 2.65 + DateTime::TimeZone::Asia::Ust_Nera 2.65 + DateTime::TimeZone::Asia::Vladivostok 2.65 + DateTime::TimeZone::Asia::Yakutsk 2.65 + DateTime::TimeZone::Asia::Yangon 2.65 + DateTime::TimeZone::Asia::Yekaterinburg 2.65 + DateTime::TimeZone::Asia::Yerevan 2.65 + DateTime::TimeZone::Atlantic::Azores 2.65 + DateTime::TimeZone::Atlantic::Bermuda 2.65 + DateTime::TimeZone::Atlantic::Canary 2.65 + DateTime::TimeZone::Atlantic::Cape_Verde 2.65 + DateTime::TimeZone::Atlantic::Faroe 2.65 + DateTime::TimeZone::Atlantic::Madeira 2.65 + DateTime::TimeZone::Atlantic::South_Georgia 2.65 + DateTime::TimeZone::Atlantic::Stanley 2.65 + DateTime::TimeZone::Australia::Adelaide 2.65 + DateTime::TimeZone::Australia::Brisbane 2.65 + DateTime::TimeZone::Australia::Broken_Hill 2.65 + DateTime::TimeZone::Australia::Darwin 2.65 + DateTime::TimeZone::Australia::Eucla 2.65 + DateTime::TimeZone::Australia::Hobart 2.65 + DateTime::TimeZone::Australia::Lindeman 2.65 + DateTime::TimeZone::Australia::Lord_Howe 2.65 + DateTime::TimeZone::Australia::Melbourne 2.65 + DateTime::TimeZone::Australia::Perth 2.65 + DateTime::TimeZone::Australia::Sydney 2.65 + DateTime::TimeZone::Catalog 2.65 + DateTime::TimeZone::Europe::Andorra 2.65 + DateTime::TimeZone::Europe::Astrakhan 2.65 + DateTime::TimeZone::Europe::Athens 2.65 + DateTime::TimeZone::Europe::Belgrade 2.65 + DateTime::TimeZone::Europe::Berlin 2.65 + DateTime::TimeZone::Europe::Brussels 2.65 + DateTime::TimeZone::Europe::Bucharest 2.65 + DateTime::TimeZone::Europe::Budapest 2.65 + DateTime::TimeZone::Europe::Chisinau 2.65 + DateTime::TimeZone::Europe::Dublin 2.65 + DateTime::TimeZone::Europe::Gibraltar 2.65 + DateTime::TimeZone::Europe::Helsinki 2.65 + DateTime::TimeZone::Europe::Istanbul 2.65 + DateTime::TimeZone::Europe::Kaliningrad 2.65 + DateTime::TimeZone::Europe::Kirov 2.65 + DateTime::TimeZone::Europe::Kyiv 2.65 + DateTime::TimeZone::Europe::Lisbon 2.65 + DateTime::TimeZone::Europe::London 2.65 + DateTime::TimeZone::Europe::Madrid 2.65 + DateTime::TimeZone::Europe::Malta 2.65 + DateTime::TimeZone::Europe::Minsk 2.65 + DateTime::TimeZone::Europe::Moscow 2.65 + DateTime::TimeZone::Europe::Paris 2.65 + DateTime::TimeZone::Europe::Prague 2.65 + DateTime::TimeZone::Europe::Riga 2.65 + DateTime::TimeZone::Europe::Rome 2.65 + DateTime::TimeZone::Europe::Samara 2.65 + DateTime::TimeZone::Europe::Saratov 2.65 + DateTime::TimeZone::Europe::Simferopol 2.65 + DateTime::TimeZone::Europe::Sofia 2.65 + DateTime::TimeZone::Europe::Tallinn 2.65 + DateTime::TimeZone::Europe::Tirane 2.65 + DateTime::TimeZone::Europe::Ulyanovsk 2.65 + DateTime::TimeZone::Europe::Vienna 2.65 + DateTime::TimeZone::Europe::Vilnius 2.65 + DateTime::TimeZone::Europe::Volgograd 2.65 + DateTime::TimeZone::Europe::Warsaw 2.65 + DateTime::TimeZone::Europe::Zurich 2.65 + DateTime::TimeZone::Floating 2.65 + DateTime::TimeZone::Indian::Chagos 2.65 + DateTime::TimeZone::Indian::Maldives 2.65 + DateTime::TimeZone::Indian::Mauritius 2.65 + DateTime::TimeZone::Local 2.65 + DateTime::TimeZone::Local::Android 2.65 + DateTime::TimeZone::Local::Unix 2.65 + DateTime::TimeZone::Local::VMS 2.65 + DateTime::TimeZone::OffsetOnly 2.65 + DateTime::TimeZone::OlsonDB 2.65 + DateTime::TimeZone::OlsonDB::Change 2.65 + DateTime::TimeZone::OlsonDB::Observance 2.65 + DateTime::TimeZone::OlsonDB::Rule 2.65 + DateTime::TimeZone::OlsonDB::Zone 2.65 + DateTime::TimeZone::Pacific::Apia 2.65 + DateTime::TimeZone::Pacific::Auckland 2.65 + DateTime::TimeZone::Pacific::Bougainville 2.65 + DateTime::TimeZone::Pacific::Chatham 2.65 + DateTime::TimeZone::Pacific::Easter 2.65 + DateTime::TimeZone::Pacific::Efate 2.65 + DateTime::TimeZone::Pacific::Fakaofo 2.65 + DateTime::TimeZone::Pacific::Fiji 2.65 + DateTime::TimeZone::Pacific::Galapagos 2.65 + DateTime::TimeZone::Pacific::Gambier 2.65 + DateTime::TimeZone::Pacific::Guadalcanal 2.65 + DateTime::TimeZone::Pacific::Guam 2.65 + DateTime::TimeZone::Pacific::Honolulu 2.65 + DateTime::TimeZone::Pacific::Kanton 2.65 + DateTime::TimeZone::Pacific::Kiritimati 2.65 + DateTime::TimeZone::Pacific::Kosrae 2.65 + DateTime::TimeZone::Pacific::Kwajalein 2.65 + DateTime::TimeZone::Pacific::Marquesas 2.65 + DateTime::TimeZone::Pacific::Nauru 2.65 + DateTime::TimeZone::Pacific::Niue 2.65 + DateTime::TimeZone::Pacific::Norfolk 2.65 + DateTime::TimeZone::Pacific::Noumea 2.65 + DateTime::TimeZone::Pacific::Pago_Pago 2.65 + DateTime::TimeZone::Pacific::Palau 2.65 + DateTime::TimeZone::Pacific::Pitcairn 2.65 + DateTime::TimeZone::Pacific::Port_Moresby 2.65 + DateTime::TimeZone::Pacific::Rarotonga 2.65 + DateTime::TimeZone::Pacific::Tahiti 2.65 + DateTime::TimeZone::Pacific::Tarawa 2.65 + DateTime::TimeZone::Pacific::Tongatapu 2.65 + DateTime::TimeZone::UTC 2.65 + requirements: + Class::Singleton 1.03 + Cwd 3 + ExtUtils::MakeMaker 0 + File::Basename 0 + File::Compare 0 + File::Find 0 + File::Spec 0 + List::Util 1.33 + Module::Runtime 0 + Params::ValidationCompiler 0.13 + Specio::Library::Builtins 0 + Specio::Library::String 0 + Try::Tiny 0 + constant 0 + namespace::autoclean 0 + parent 0 + perl 5.008004 + strict 0 + warnings 0 + Devel-CheckLib-1.16 + pathname: M/MA/MATTN/Devel-CheckLib-1.16.tar.gz + provides: + Devel::CheckLib 1.16 + requirements: + Exporter 0 + ExtUtils::MakeMaker 0 + File::Spec 0 + File::Temp 0.16 + perl 5.004050 + Devel-Confess-0.009004 + pathname: H/HA/HAARG/Devel-Confess-0.009004.tar.gz + provides: + Devel::Confess 0.009004 + Devel::Confess::Builtin 0.009004 + Devel::Confess::Source undef + Devel::Confess::_Util undef + requirements: + Carp 0 + ExtUtils::MakeMaker 0 + Scalar::Util 0 + perl 5.006 + Devel-GlobalDestruction-0.14 + pathname: H/HA/HAARG/Devel-GlobalDestruction-0.14.tar.gz + provides: + Devel::GlobalDestruction 0.14 + requirements: + ExtUtils::MakeMaker 0 + Sub::Exporter::Progressive 0.001011 + perl 5.006 + Devel-Hide-0.0015 + pathname: D/DC/DCANTRELL/Devel-Hide-0.0015.tar.gz + provides: + Devel::Hide 0.0015 + requirements: + ExtUtils::MakeMaker 0 + File::Temp 0 + perl 5.006001 + Devel-OverloadInfo-0.007 + pathname: I/IL/ILMARI/Devel-OverloadInfo-0.007.tar.gz + provides: + Devel::OverloadInfo 0.007 + requirements: + B 0 + Exporter 5.57 + ExtUtils::MakeMaker 0 + MRO::Compat 0 + Package::Stash 0.14 + Scalar::Util 0 + Sub::Util 1.40 + Text::ParseWords 0 + overload 0 + perl 5.006 + strict 0 + warnings 0 + Devel-PartialDump-0.20 + pathname: E/ET/ETHER/Devel-PartialDump-0.20.tar.gz + provides: + Devel::PartialDump 0.20 + requirements: + Carp 0 + Class::Tiny 0 + ExtUtils::MakeMaker 0 + Scalar::Util 0 + Sub::Exporter 0 + namespace::clean 0.19 + perl 5.006001 + strict 0 + warnings 0 + Devel-StackTrace-2.05 + pathname: D/DR/DROLSKY/Devel-StackTrace-2.05.tar.gz + provides: + Devel::StackTrace 2.05 + Devel::StackTrace::Frame 2.05 + requirements: + ExtUtils::MakeMaker 0 + File::Spec 0 + Scalar::Util 0 + overload 0 + perl 5.006 + strict 0 + warnings 0 + Devel-StackTrace-AsHTML-0.15 + pathname: M/MI/MIYAGAWA/Devel-StackTrace-AsHTML-0.15.tar.gz + provides: + Devel::StackTrace::AsHTML 0.15 + requirements: + Devel::StackTrace 0 + ExtUtils::MakeMaker 0 + Digest-HMAC-1.05 + pathname: A/AR/ARODLAND/Digest-HMAC-1.05.tar.gz + provides: + Digest::HMAC 1.05 + Digest::HMAC_MD5 1.05 + Digest::HMAC_SHA1 1.05 + requirements: + Digest::MD5 2 + Digest::SHA 1 + ExtUtils::MakeMaker 0 + perl 5.008001 + Digest-JHash-0.10 + pathname: S/SH/SHLOMIF/Digest-JHash-0.10.tar.gz + provides: + Digest::JHash 0.10 + requirements: + DynaLoader 0 + Exporter 0 + ExtUtils::MakeMaker 0 + perl 5.008 + strict 0 + vars 0 + warnings 0 + Dist-CheckConflicts-0.11 + pathname: D/DO/DOY/Dist-CheckConflicts-0.11.tar.gz + provides: + Dist::CheckConflicts 0.11 + requirements: + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 6.30 + Module::Runtime 0.009 + base 0 + strict 0 + warnings 0 + Dist-Data-0.007 + pathname: G/GE/GETTY/Dist-Data-0.007.tar.gz + provides: + Dist::Data 0.007 + requirements: + Archive::Any 0.0932 + CPAN::Meta 2.113640 + DateTime::Format::Epoch 0.13 + Dist::Metadata 0.922 + ExtUtils::MakeMaker 0 + File::Find::Object v0.2.3 + File::Temp 0.22 + Module::Metadata 0 + Moo 0.009013 + Dist-Metadata-0.927 + pathname: R/RW/RWSTAUNER/Dist-Metadata-0.927.tar.gz + provides: + Dist::Metadata 0.927 + Dist::Metadata::Archive 0.927 + Dist::Metadata::Dir 0.927 + Dist::Metadata::Dist 0.927 + Dist::Metadata::Struct 0.927 + Dist::Metadata::Tar 0.927 + Dist::Metadata::Zip 0.927 + requirements: + Archive::Tar 1 + Archive::Zip 1.30 + CPAN::DistnameInfo 0.12 + CPAN::Meta 2.1 + Carp 0 + Digest 1.03 + Digest::MD5 2 + Digest::SHA 5 + ExtUtils::MakeMaker 0 + File::Basename 0 + File::Find 0 + File::Spec::Native 1.002 + File::Temp 0.19 + List::Util 0 + Module::Metadata 0 + Path::Class 0.24 + Try::Tiny 0.09 + parent 0 + perl 5.006 + strict 0 + warnings 0 + EV-4.34 + pathname: M/ML/MLEHMANN/EV-4.34.tar.gz + provides: + EV 4.34 + EV::MakeMaker undef + requirements: + Canary::Stability 0 + ExtUtils::MakeMaker 6.52 + common::sense 0 + ElasticSearchX-Model-2.0.1 + pathname: O/OA/OALDERS/ElasticSearchX-Model-2.0.1.tar.gz + provides: + ElasticSearchX::Model v2.0.1 + ElasticSearchX::Model::Bulk v2.0.1 + ElasticSearchX::Model::Document v2.0.1 + ElasticSearchX::Model::Document::EmbeddedRole v2.0.1 + ElasticSearchX::Model::Document::Mapping v2.0.1 + ElasticSearchX::Model::Document::Role v2.0.1 + ElasticSearchX::Model::Document::Set v2.0.1 + ElasticSearchX::Model::Document::Trait::Attribute v2.0.1 + ElasticSearchX::Model::Document::Trait::Class v2.0.1 + ElasticSearchX::Model::Document::Trait::Class::ID v2.0.1 + ElasticSearchX::Model::Document::Trait::Class::Timestamp v2.0.1 + ElasticSearchX::Model::Document::Trait::Class::Version v2.0.1 + ElasticSearchX::Model::Document::Trait::Field::ID v2.0.1 + ElasticSearchX::Model::Document::Trait::Field::TTL v2.0.1 + ElasticSearchX::Model::Document::Trait::Field::Timestamp v2.0.1 + ElasticSearchX::Model::Document::Trait::Field::Version v2.0.1 + ElasticSearchX::Model::Document::Types v2.0.1 + ElasticSearchX::Model::Index v2.0.1 + ElasticSearchX::Model::Role v2.0.1 + ElasticSearchX::Model::Scroll v2.0.1 + ElasticSearchX::Model::Trait::Class v2.0.1 + ElasticSearchX::Model::Tutorial v2.0.1 + ElasticSearchX::Model::Util v2.0.1 + requirements: + Carp 0 + Class::Load 0 + DateTime 0 + DateTime::Format::Epoch::Unix 0 + DateTime::Format::ISO8601 0 + Digest::SHA 0 + Eval::Closure 0 + ExtUtils::MakeMaker 0 + JSON::MaybeXS 0 + List::MoreUtils 0 + List::Util 0 + Module::Find 0 + Moose 2.02 + Moose::Exporter 0 + Moose::Role 0 + Moose::Util::TypeConstraints 0 + MooseX::Attribute::Chained v1.0.1 + MooseX::Attribute::ChainedClone 0 + MooseX::Attribute::Deflator v2.2.0 + MooseX::Attribute::Deflator::Moose 0 + MooseX::Attribute::LazyInflator::Meta::Role::Attribute 0 + MooseX::Types 0 + MooseX::Types::ElasticSearch v0.0.4 + MooseX::Types::Moose 0 + MooseX::Types::Structured 0 + Scalar::Util 0 + Search::Elasticsearch 2.02 + Sub::Exporter 0 + perl 5.006 + strict 0 + version 0 + warnings 0 + Email-Abstract-3.010 + pathname: R/RJ/RJBS/Email-Abstract-3.010.tar.gz + provides: + Email::Abstract 3.010 + Email::Abstract::EmailMIME 3.010 + Email::Abstract::EmailSimple 3.010 + Email::Abstract::MIMEEntity 3.010 + Email::Abstract::MailInternet 3.010 + Email::Abstract::MailMessage 3.010 + Email::Abstract::Plugin 3.010 + requirements: + Carp 0 + Email::Simple 1.998 + ExtUtils::MakeMaker 6.78 + MRO::Compat 0 + Module::Pluggable 1.5 + Scalar::Util 0 + perl 5.006 + strict 0 + warnings 0 + Email-Address-XS-1.05 + pathname: P/PA/PALI/Email-Address-XS-1.05.tar.gz + provides: + Email::Address::XS 1.05 + requirements: + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 0 + XSLoader 0 + base 0 + overload 0 + perl 5.006000 + strict 0 + warnings 0 + Email-Date-Format-1.008 + pathname: R/RJ/RJBS/Email-Date-Format-1.008.tar.gz + provides: + Email::Date::Format 1.008 + requirements: + Exporter 5.57 + ExtUtils::MakeMaker 6.78 + Time::Local 1.27 + perl 5.012 + warnings 0 + Email-Sender-2.601 + pathname: R/RJ/RJBS/Email-Sender-2.601.tar.gz + provides: + Email::Sender 2.601 + Email::Sender::Failure 2.601 + Email::Sender::Failure::Multi 2.601 + Email::Sender::Failure::Permanent 2.601 + Email::Sender::Failure::Temporary 2.601 + Email::Sender::Manual 2.601 + Email::Sender::Manual::QuickStart 2.601 + Email::Sender::Role::CommonSending 2.601 + Email::Sender::Role::HasMessage 2.601 + Email::Sender::Simple 2.601 + Email::Sender::Success 2.601 + Email::Sender::Success::Partial 2.601 + Email::Sender::Transport 2.601 + Email::Sender::Transport::DevNull 2.601 + Email::Sender::Transport::Failable 2.601 + Email::Sender::Transport::Maildir 2.601 + Email::Sender::Transport::Mbox 2.601 + Email::Sender::Transport::Print 2.601 + Email::Sender::Transport::SMTP 2.601 + Email::Sender::Transport::SMTP::Persistent 2.601 + Email::Sender::Transport::Sendmail 2.601 + Email::Sender::Transport::Test 2.601 + Email::Sender::Transport::Wrapper 2.601 + Email::Sender::Util 2.601 + requirements: + Carp 0 + Email::Abstract 3.006 + Email::Address::XS 0 + Email::Simple 1.998 + ExtUtils::MakeMaker 6.78 + Fcntl 0 + File::Basename 0 + File::Path 2.06 + File::Spec 0 + IO::File 1.11 + IO::Handle 0 + List::Util 1.45 + Module::Runtime 0 + Moo 2.000000 + Moo::Role 0 + MooX::Types::MooseLike 0.15 + MooX::Types::MooseLike::Base 0 + Net::SMTP 3.07 + Scalar::Util 0 + Sub::Exporter 0 + Sub::Exporter::Util 0 + Sys::Hostname 0 + Throwable::Error 0.200003 + Try::Tiny 0 + perl 5.012 + strict 0 + utf8 0 + warnings 0 + Email-Simple-2.218 + pathname: R/RJ/RJBS/Email-Simple-2.218.tar.gz + provides: + Email::Simple 2.218 + Email::Simple::Creator 2.218 + Email::Simple::Header 2.218 + requirements: + Carp 0 + Email::Date::Format 0 + ExtUtils::MakeMaker 6.78 + perl 5.012 + strict 0 + warnings 0 + Email-Valid-1.204 + pathname: R/RJ/RJBS/Email-Valid-1.204.tar.gz + provides: + Email::Valid 1.204 + requirements: + ExtUtils::MakeMaker 0 + Mail::Address 0 + Net::DNS 0 + Scalar::Util 0 + Test::More 0 + perl 5.006 + Encode-3.21 + pathname: D/DA/DANKOGAI/Encode-3.21.tar.gz + provides: + Encode 3.21 + Encode::Alias 2.25 + Encode::Byte 2.04 + Encode::CJKConstants 2.02 + Encode::CN 2.03 + Encode::CN::HZ 2.10 + Encode::Config 2.05 + Encode::EBCDIC 2.02 + Encode::Encoder 2.03 + Encode::Encoding 2.08 + Encode::GSM0338 2.10 + Encode::Guess 2.08 + Encode::JP 2.05 + Encode::JP::H2Z 2.02 + Encode::JP::JIS7 2.08 + Encode::KR 2.03 + Encode::KR::2022_KR 2.04 + Encode::MIME::Header 2.29 + Encode::MIME::Header::ISO_2022_JP 1.09 + Encode::MIME::Name 1.03 + Encode::Symbol 2.02 + Encode::TW 2.03 + Encode::UTF_EBCDIC 3.21 + Encode::Unicode 2.20 + Encode::Unicode::UTF7 2.10 + Encode::XS 3.21 + Encode::utf8 3.21 + encoding 3.00 + requirements: + Exporter 5.57 + ExtUtils::MakeMaker 0 + Storable 0 + parent 0.221 + Encode-Locale-1.05 + pathname: G/GA/GAAS/Encode-Locale-1.05.tar.gz + provides: + Encode::Locale 1.05 + requirements: + Encode 2 + Encode::Alias 0 + ExtUtils::MakeMaker 0 + perl 5.008 + Encoding-FixLatin-1.04 + pathname: G/GR/GRANTM/Encoding-FixLatin-1.04.tar.gz + provides: + Encoding::FixLatin 1.04 + requirements: + ExtUtils::MakeMaker 6.30 + Test::More 0.90 + Encoding-FixLatin-XS-1.02 + pathname: G/GR/GRANTM/Encoding-FixLatin-XS-1.02.tar.gz + provides: + Encoding::FixLatin::XS 1.02 + requirements: + ExtUtils::MakeMaker 0 + perl 5.014 + Eval-Closure-0.14 + pathname: D/DO/DOY/Eval-Closure-0.14.tar.gz + provides: + Eval::Closure 0.14 + requirements: + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 0 + Scalar::Util 0 + constant 0 + overload 0 + strict 0 + warnings 0 + Exception-Class-1.45 + pathname: D/DR/DROLSKY/Exception-Class-1.45.tar.gz + provides: + Exception::Class 1.45 + Exception::Class::Base 1.45 + requirements: + Carp 0 + Class::Data::Inheritable 0.02 + Devel::StackTrace 2.00 + ExtUtils::MakeMaker 0 + Scalar::Util 0 + base 0 + overload 0 + perl 5.008001 + strict 0 + warnings 0 + Exporter-5.78 + pathname: T/TO/TODDR/Exporter-5.78.tar.gz + provides: + Exporter 5.78 + Exporter::Heavy 5.78 + requirements: + Carp 1.05 + ExtUtils::MakeMaker 0 + Exporter-Tiny-1.006002 + pathname: T/TO/TOBYINK/Exporter-Tiny-1.006002.tar.gz + provides: + Exporter::Shiny 1.006002 + Exporter::Tiny 1.006002 + requirements: + ExtUtils::MakeMaker 6.17 + perl 5.006001 + ExtUtils-Config-0.010 + pathname: L/LE/LEONT/ExtUtils-Config-0.010.tar.gz + provides: + ExtUtils::Config 0.010 + ExtUtils::Config::MakeMaker 0.010 + requirements: + Data::Dumper 0 + ExtUtils::MakeMaker 0 + ExtUtils::MakeMaker::Config 0 + perl 5.006 + strict 0 + warnings 0 + ExtUtils-Depends-0.8002 + pathname: E/ET/ETJ/ExtUtils-Depends-0.8002.tar.gz + provides: + ExtUtils::Depends 0.8002 + requirements: + Data::Dumper 0 + ExtUtils::MakeMaker 7.44 + File::Spec 0 + IO::File 0 + perl 5.006 + ExtUtils-Helpers-0.028 + pathname: L/LE/LEONT/ExtUtils-Helpers-0.028.tar.gz + provides: + ExtUtils::Helpers 0.028 + ExtUtils::Helpers::Unix 0.028 + ExtUtils::Helpers::VMS 0.028 + ExtUtils::Helpers::Windows 0.028 + requirements: + Carp 0 + Exporter 5.57 + ExtUtils::MakeMaker 0 + File::Basename 0 + File::Copy 0 + File::Spec::Functions 0 + Text::ParseWords 3.24 + strict 0 + warnings 0 + ExtUtils-InstallPaths-0.014 + pathname: L/LE/LEONT/ExtUtils-InstallPaths-0.014.tar.gz + provides: + ExtUtils::InstallPaths 0.014 + requirements: + Carp 0 + ExtUtils::Config 0.009 + ExtUtils::MakeMaker 0 + File::Spec 0 + perl 5.008 + strict 0 + warnings 0 + ExtUtils-MakeMaker-7.74 + pathname: B/BI/BINGOS/ExtUtils-MakeMaker-7.74.tar.gz + provides: + ExtUtils::Command 7.74 + ExtUtils::Command::MM 7.74 + ExtUtils::Liblist 7.74 + ExtUtils::Liblist::Kid 7.74 + ExtUtils::MM 7.74 + ExtUtils::MM_AIX 7.74 + ExtUtils::MM_Any 7.74 + ExtUtils::MM_BeOS 7.74 + ExtUtils::MM_Cygwin 7.74 + ExtUtils::MM_DOS 7.74 + ExtUtils::MM_Darwin 7.74 + ExtUtils::MM_MacOS 7.74 + ExtUtils::MM_NW5 7.74 + ExtUtils::MM_OS2 7.74 + ExtUtils::MM_OS390 7.74 + ExtUtils::MM_QNX 7.74 + ExtUtils::MM_UWIN 7.74 + ExtUtils::MM_Unix 7.74 + ExtUtils::MM_VMS 7.74 + ExtUtils::MM_VOS 7.74 + ExtUtils::MM_Win32 7.74 + ExtUtils::MM_Win95 7.74 + ExtUtils::MY 7.74 + ExtUtils::MakeMaker 7.74 + ExtUtils::MakeMaker::Config 7.74 + ExtUtils::MakeMaker::Locale 7.74 + ExtUtils::MakeMaker::_version 7.74 + ExtUtils::MakeMaker::charstar 7.74 + ExtUtils::MakeMaker::version 7.74 + ExtUtils::MakeMaker::version::regex 7.74 + ExtUtils::MakeMaker::version::vpp 7.74 + ExtUtils::Mkbootstrap 7.74 + ExtUtils::Mksymlists 7.74 + ExtUtils::testlib 7.74 + MM 7.74 + MY 7.74 + requirements: + Data::Dumper 0 + Encode 0 + File::Basename 0 + File::Spec 0.8 + Pod::Man 0 + perl 5.006 + ExtUtils-MakeMaker-CPANfile-0.09 + pathname: I/IS/ISHIGAKI/ExtUtils-MakeMaker-CPANfile-0.09.tar.gz + provides: + ExtUtils::MakeMaker::CPANfile 0.09 + requirements: + CPAN::Meta::Converter 2.141170 + Cwd 0 + ExtUtils::MakeMaker 6.17 + File::Path 0 + Module::CPANfile 0 + Test::More 0.88 + version 0.76 + File-Find-Object-0.3.9 + pathname: S/SH/SHLOMIF/File-Find-Object-0.3.9.tar.gz + provides: + File::Find::Object v0.3.9 + File::Find::Object::Base v0.3.9 + File::Find::Object::DeepPath v0.3.9 + File::Find::Object::PathComp v0.3.9 + File::Find::Object::Result v0.3.9 + File::Find::Object::TopPath v0.3.9 + requirements: + Carp 0 + Class::XSAccessor 0 + ExtUtils::MakeMaker 0 + Fcntl 0 + File::Spec 0 + List::Util 0 + Module::Build 0.28 + integer 0 + parent 0 + perl 5.008 + strict 0 + warnings 0 + File-Find-Rule-0.34 + pathname: R/RC/RCLAMP/File-Find-Rule-0.34.tar.gz + provides: + File::Find::Rule 0.34 + File::Find::Rule::Test::ATeam undef + requirements: + ExtUtils::MakeMaker 0 + File::Find 0 + File::Spec 0 + Number::Compare 0 + Test::More 0 + Text::Glob 0.07 + File-Find-Rule-Perl-1.16 + pathname: E/ET/ETHER/File-Find-Rule-Perl-1.16.tar.gz + provides: + File::Find::Rule::Perl 1.16 + requirements: + ExtUtils::MakeMaker 0 + File::Find::Rule 0.20 + File::Spec 0.82 + Params::Util 0.38 + Parse::CPAN::Meta 1.38 + perl 5.006 + File-HomeDir-1.006 + pathname: R/RE/REHSACK/File-HomeDir-1.006.tar.gz + provides: + File::HomeDir 1.006 + File::HomeDir::Darwin 1.006 + File::HomeDir::Darwin::Carbon 1.006 + File::HomeDir::Darwin::Cocoa 1.006 + File::HomeDir::Driver 1.006 + File::HomeDir::FreeDesktop 1.006 + File::HomeDir::MacOS9 1.006 + File::HomeDir::Test 1.006 + File::HomeDir::Unix 1.006 + File::HomeDir::Windows 1.006 + requirements: + Carp 0 + Cwd 3.12 + ExtUtils::MakeMaker 0 + File::Basename 0 + File::Path 2.01 + File::Spec 3.12 + File::Temp 0.19 + File::Which 0.05 + POSIX 0 + perl 5.008003 + File-Listing-6.16 + pathname: P/PL/PLICEASE/File-Listing-6.16.tar.gz + provides: + File::Listing 6.16 + File::Listing::apache 6.16 + File::Listing::dosftp 6.16 + File::Listing::netware 6.16 + File::Listing::unix 6.16 + File::Listing::vms 6.16 + requirements: + Exporter 5.57 + ExtUtils::MakeMaker 0 + HTTP::Date 0 + perl 5.006 + File-MMagic-1.30 + pathname: K/KN/KNOK/File-MMagic-1.30.tar.gz + provides: + File::MMagic 1.30 + requirements: + ExtUtils::MakeMaker 0 + File-Next-1.18 + pathname: P/PE/PETDANCE/File-Next-1.18.tar.gz + provides: + File::Next 1.18 + requirements: + ExtUtils::MakeMaker 0 + File::Copy 0 + File::Spec 0 + File::Temp 0.22 + Test::More 0.88 + File-ShareDir-1.118 + pathname: R/RE/REHSACK/File-ShareDir-1.118.tar.gz + provides: + File::ShareDir 1.118 + requirements: + Carp 0 + Class::Inspector 1.12 + ExtUtils::MakeMaker 0 + File::ShareDir::Install 0.13 + File::Spec 0.80 + perl 5.008001 + warnings 0 + File-ShareDir-Install-0.14 + pathname: E/ET/ETHER/File-ShareDir-Install-0.14.tar.gz + provides: + File::ShareDir::Install 0.14 + requirements: + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 0 + File::Spec 0 + IO::Dir 0 + perl 5.006 + strict 0 + warnings 0 + File-Spec-Native-1.004 + pathname: R/RW/RWSTAUNER/File-Spec-Native-1.004.tar.gz + provides: + File::Spec::Native 1.004 + requirements: + ExtUtils::MakeMaker 0 + File::Spec 0 + perl 5.006 + strict 0 + warnings 0 + File-Which-1.27 + pathname: P/PL/PLICEASE/File-Which-1.27.tar.gz + provides: + File::Which 1.27 + requirements: + ExtUtils::MakeMaker 0 + base 0 + perl 5.006 + File-XDG-1.03 + pathname: P/PL/PLICEASE/File-XDG-1.03.tar.gz + provides: + File::XDG 1.03 + requirements: + ExtUtils::MakeMaker 0 + File::Path 2.07 + Path::Tiny 0 + Ref::Util 0 + perl 5.006 + File-pushd-1.016 + pathname: D/DA/DAGOLDEN/File-pushd-1.016.tar.gz + provides: + File::pushd 1.016 + requirements: + Carp 0 + Cwd 0 + Exporter 0 + ExtUtils::MakeMaker 6.17 + File::Path 0 + File::Spec 0 + File::Temp 0 + overload 0 + perl 5.006 + strict 0 + warnings 0 + Filesys-Notify-Simple-0.14 + pathname: M/MI/MIYAGAWA/Filesys-Notify-Simple-0.14.tar.gz + provides: + Filesys::Notify::Simple 0.14 + requirements: + ExtUtils::MakeMaker 0 + perl 5.008001 + Getopt-Long-2.58 + pathname: J/JV/JV/Getopt-Long-2.58.tar.gz + provides: + Getopt::Long 2.58 + Getopt::Long::Parser 2.58 + requirements: + ExtUtils::MakeMaker 0 + Pod::Usage 1.14 + Getopt-Long-Descriptive-0.116 + pathname: R/RJ/RJBS/Getopt-Long-Descriptive-0.116.tar.gz + provides: + Getopt::Long::Descriptive 0.116 + Getopt::Long::Descriptive::Opts 0.116 + Getopt::Long::Descriptive::Usage 0.116 + requirements: + Carp 0 + ExtUtils::MakeMaker 6.78 + File::Basename 0 + Getopt::Long 2.55 + List::Util 0 + Params::Validate 0.97 + Scalar::Util 0 + Sub::Exporter 0.972 + Sub::Exporter::Util 0 + overload 0 + perl 5.012 + strict 0 + warnings 0 + Gravatar-URL-1.07 + pathname: M/MS/MSCHWERN/Gravatar-URL-1.07.tar.gz + provides: + Gravatar::URL 1.07 + Libravatar::URL 1.07 + Unicornify::URL 1.07 + requirements: + Carp 0 + Digest::MD5 0 + Digest::SHA 0 + Net::DNS 1.01 + Test::MockRandom 1.01 + Test::More 0.4 + Test::Warn 0.11 + URI::Escape 0 + parent 0 + perl v5.6.0 + HTML-Parser-3.83 + pathname: O/OA/OALDERS/HTML-Parser-3.83.tar.gz + provides: + HTML::Entities 3.83 + HTML::Filter 3.83 + HTML::HeadParser 3.83 + HTML::LinkExtor 3.83 + HTML::Parser 3.83 + HTML::PullParser 3.83 + HTML::TokeParser 3.83 + requirements: + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 6.52 + HTML::Tagset 0 + HTTP::Headers 0 + IO::File 0 + URI 0 + URI::URL 0 + XSLoader 0 + strict 0 + HTML-Tagset-3.24 + pathname: P/PE/PETDANCE/HTML-Tagset-3.24.tar.gz + provides: + HTML::Tagset 3.24 + requirements: + ExtUtils::MakeMaker 6.46 + perl 5.010001 + HTTP-Body-1.23 + pathname: G/GE/GETTY/HTTP-Body-1.23.tar.gz + provides: + HTTP::Body 1.23 + HTTP::Body::MultiPart 1.23 + HTTP::Body::OctetStream 1.23 + HTTP::Body::UrlEncoded 1.23 + HTTP::Body::XForms 1.23 + HTTP::Body::XFormsMultipart 1.23 + requirements: + Carp 0 + Digest::MD5 0 + ExtUtils::MakeMaker 0 + File::Temp 0.14 + HTTP::Headers 0 + IO::File 1.14 + HTTP-Cookies-6.11 + pathname: O/OA/OALDERS/HTTP-Cookies-6.11.tar.gz + provides: + HTTP::Cookies 6.11 + HTTP::Cookies::Microsoft 6.11 + HTTP::Cookies::Netscape 6.11 + requirements: + Carp 0 + ExtUtils::MakeMaker 0 + HTTP::Date 6 + HTTP::Headers::Util 6 + HTTP::Request 0 + locale 0 + perl 5.008001 + strict 0 + HTTP-Date-6.06 + pathname: O/OA/OALDERS/HTTP-Date-6.06.tar.gz + provides: + HTTP::Date 6.06 + requirements: + Exporter 0 + ExtUtils::MakeMaker 0 + Time::Local 1.28 + Time::Zone 0 + perl 5.006002 + strict 0 + HTTP-Entity-Parser-0.25 + pathname: K/KA/KAZEBURO/HTTP-Entity-Parser-0.25.tar.gz + provides: + HTTP::Entity::Parser 0.25 + HTTP::Entity::Parser::JSON undef + HTTP::Entity::Parser::MultiPart undef + HTTP::Entity::Parser::OctetStream undef + HTTP::Entity::Parser::UrlEncoded undef + requirements: + Encode 0 + File::Temp 0 + HTTP::MultiPartParser 0 + Hash::MultiValue 0 + JSON::MaybeXS 1.003007 + Module::Build::Tiny 0.035 + Module::Load 0 + Stream::Buffered 0 + WWW::Form::UrlEncoded 0.23 + perl 5.008001 + HTTP-Headers-Fast-0.22 + pathname: T/TO/TOKUHIROM/HTTP-Headers-Fast-0.22.tar.gz + provides: + HTTP::Headers::Fast 0.22 + requirements: + HTTP::Date 0 + Module::Build::Tiny 0.035 + perl 5.008001 + HTTP-Message-7.00 + pathname: O/OA/OALDERS/HTTP-Message-7.00.tar.gz + provides: + HTTP::Config 7.00 + HTTP::Headers 7.00 + HTTP::Headers::Auth 7.00 + HTTP::Headers::ETag 7.00 + HTTP::Headers::Util 7.00 + HTTP::Message 7.00 + HTTP::Request 7.00 + HTTP::Request::Common 7.00 + HTTP::Response 7.00 + HTTP::Status 7.00 + requirements: + Carp 0 + Clone 0.46 + Compress::Raw::Bzip2 0 + Compress::Raw::Zlib 2.062 + Encode 3.01 + Encode::Locale 1 + Exporter 5.57 + ExtUtils::MakeMaker 0 + File::Spec 0 + HTTP::Date 6 + IO::Compress::Bzip2 2.021 + IO::Compress::Deflate 0 + IO::Compress::Gzip 0 + IO::HTML 0 + IO::Uncompress::Inflate 0 + IO::Uncompress::RawInflate 0 + LWP::MediaTypes 6 + MIME::Base64 2.1 + MIME::QuotedPrint 0 + URI 1.10 + parent 0 + perl 5.008001 + strict 0 + warnings 0 + HTTP-MultiPartParser-0.02 + pathname: C/CH/CHANSEN/HTTP-MultiPartParser-0.02.tar.gz + provides: + HTTP::MultiPartParser 0.02 + requirements: + Carp 0 + ExtUtils::MakeMaker 6.59 + Scalar::Util 0 + Test::Deep 0 + Test::More 0.88 + perl 5.008001 + HTTP-Negotiate-6.01 + pathname: G/GA/GAAS/HTTP-Negotiate-6.01.tar.gz + provides: + HTTP::Negotiate 6.01 + requirements: + ExtUtils::MakeMaker 0 + HTTP::Headers 6 + perl 5.008001 + HTTP-Thin-0.006 + pathname: P/PE/PERIGRIN/HTTP-Thin-0.006.tar.gz + provides: + HTTP::Thin 0.006 + requirements: + Class::Method::Modifiers 0 + ExtUtils::MakeMaker 6.30 + HTTP::Response 0 + HTTP::Tiny 0 + Hash::MultiValue 0 + Safe::Isa 0 + parent 0 + warnings 0 + HTTP-Tiny-0.090 + pathname: H/HA/HAARG/HTTP-Tiny-0.090.tar.gz + provides: + HTTP::Tiny 0.090 + requirements: + Carp 0 + ExtUtils::MakeMaker 6.17 + Fcntl 0 + IO::Socket 0 + MIME::Base64 0 + Socket 0 + Time::Local 0 + bytes 0 + perl 5.006 + strict 0 + warnings 0 + Hash-Merge-0.302 + pathname: H/HE/HERMES/Hash-Merge-0.302.tar.gz + provides: + Hash::Merge 0.302 + requirements: + Clone::Choose 0.008 + ExtUtils::MakeMaker 6.64 + Scalar::Util 0 + perl 5.008001 + Hash-Merge-Simple-0.052 + pathname: H/HA/HAARG/Hash-Merge-Simple-0.052.tar.gz + provides: + Hash::Merge::Simple 0.052 + requirements: + Clone 0 + ExtUtils::MakeMaker 0 + Storable 0 + perl 5.006000 + Hash-MoreUtils-0.06 + pathname: R/RE/REHSACK/Hash-MoreUtils-0.06.tar.gz + provides: + Hash::MoreUtils 0.06 + requirements: + ExtUtils::MakeMaker 0 + perl 5.008001 + Hash-MultiValue-0.16 + pathname: A/AR/ARISTOTLE/Hash-MultiValue-0.16.tar.gz + provides: + Hash::MultiValue 0.16 + requirements: + ExtUtils::MakeMaker 0 + perl 5.008001 + IO-1.55 + pathname: T/TO/TODDR/IO-1.55.tar.gz + provides: + IO 1.55 + IO::Dir 1.55 + IO::File 1.55 + IO::Handle 1.55 + IO::Pipe 1.55 + IO::Pipe::End 1.55 + IO::Poll 1.55 + IO::Seekable 1.55 + IO::Select 1.55 + IO::Socket 1.55 + IO::Socket::INET 1.55 + IO::Socket::UNIX 1.55 + requirements: + ExtUtils::MakeMaker 0 + File::Temp 0.15 + Test::More 0 + IO-Compress-2.213 + pathname: P/PM/PMQS/IO-Compress-2.213.tar.gz + provides: + Compress::Zlib 2.213 + File::GlobMapper 1.001 + IO::Compress 2.213 + IO::Compress::Adapter::Bzip2 2.213 + IO::Compress::Adapter::Deflate 2.213 + IO::Compress::Adapter::Identity 2.213 + IO::Compress::Base 2.213 + IO::Compress::Base::Common 2.213 + IO::Compress::Bzip2 2.213 + IO::Compress::Deflate 2.213 + IO::Compress::Gzip 2.213 + IO::Compress::Gzip::Constants 2.213 + IO::Compress::RawDeflate 2.213 + IO::Compress::Zip 2.213 + IO::Compress::Zip::Constants 2.213 + IO::Compress::Zlib::Constants 2.213 + IO::Compress::Zlib::Extra 2.213 + IO::Uncompress::Adapter::Bunzip2 2.213 + IO::Uncompress::Adapter::Identity 2.213 + IO::Uncompress::Adapter::Inflate 2.213 + IO::Uncompress::AnyInflate 2.213 + IO::Uncompress::AnyUncompress 2.213 + IO::Uncompress::Base 2.213 + IO::Uncompress::Bunzip2 2.213 + IO::Uncompress::Gunzip 2.213 + IO::Uncompress::Inflate 2.213 + IO::Uncompress::RawInflate 2.213 + IO::Uncompress::Unzip 2.213 + U64 2.213 + Zlib::OldDeflate 2.213 + Zlib::OldInflate 2.213 + requirements: + Compress::Raw::Bzip2 2.213 + Compress::Raw::Zlib 2.213 + Encode 0 + ExtUtils::MakeMaker 0 + Scalar::Util 0 + Time::Local 0 + IO-File-AtomicChange-0.08 + pathname: H/HI/HIROSE/IO-File-AtomicChange-0.08.tar.gz + provides: + IO::File::AtomicChange 0.08 + requirements: + File::Copy 0 + File::Temp 0 + IO 1.39 + Module::Build::Tiny 0.039 + POSIX 0 + Path::Class 0 + Time::HiRes 0 + perl 5.008005 + IO-HTML-1.004 + pathname: C/CJ/CJM/IO-HTML-1.004.tar.gz + provides: + IO::HTML 1.004 + requirements: + Carp 0 + Encode 2.10 + Exporter 5.57 + ExtUtils::MakeMaker 0 + perl 5.008 + IO-Prompt-Tiny-0.003 + pathname: D/DA/DAGOLDEN/IO-Prompt-Tiny-0.003.tar.gz + provides: + IO::Prompt::Tiny 0.003 + requirements: + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 6.17 + perl 5.006 + strict 0 + warnings 0 + IO-Socket-IP-0.43 + pathname: P/PE/PEVANS/IO-Socket-IP-0.43.tar.gz + provides: + IO::Socket::IP 0.43 + requirements: + IO::Socket 0 + Module::Build 0.4004 + Socket 1.97 + perl 5.014 + IO-Socket-SSL-2.089 + pathname: S/SU/SULLR/IO-Socket-SSL-2.089.tar.gz + provides: + IO::Socket::SSL 2.089 + IO::Socket::SSL::Intercept 2.056 + IO::Socket::SSL::OCSP_Cache 2.089 + IO::Socket::SSL::OCSP_Resolver 2.089 + IO::Socket::SSL::PublicSuffix undef + IO::Socket::SSL::SSL_Context 2.089 + IO::Socket::SSL::SSL_HANDLE 2.089 + IO::Socket::SSL::Session_Cache 2.089 + IO::Socket::SSL::Trace 2.089 + IO::Socket::SSL::Utils 2.015 + requirements: + ExtUtils::MakeMaker 0 + Net::SSLeay 1.46 + Scalar::Util 0 + IPC-Run3-0.049 + pathname: R/RJ/RJBS/IPC-Run3-0.049.tar.gz + provides: + IPC::Run3 0.049 + requirements: + ExtUtils::MakeMaker 0 + Test::More 0.31 + Time::HiRes 0 + IPC-System-Simple-1.30 + pathname: J/JK/JKEENAN/IPC-System-Simple-1.30.tar.gz + provides: + IPC::System::Simple 1.30 + requirements: + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 0 + List::Util 0 + POSIX 0 + Scalar::Util 0 + constant 0 + perl 5.006 + re 0 + strict 0 + warnings 0 + Import-Into-1.002005 + pathname: H/HA/HAARG/Import-Into-1.002005.tar.gz + provides: + Import::Into 1.002005 + requirements: + ExtUtils::MakeMaker 0 + Module::Runtime 0 + perl 5.006 + strict 0 + warnings 0 + JSON-4.10 + pathname: I/IS/ISHIGAKI/JSON-4.10.tar.gz + provides: + JSON 4.10 + JSON::Backend::PP 4.10 + requirements: + ExtUtils::MakeMaker 0 + Test::More 0 + JSON-MaybeXS-1.004008 + pathname: E/ET/ETHER/JSON-MaybeXS-1.004008.tar.gz + provides: + JSON::MaybeXS 1.004008 + requirements: + Carp 0 + Cpanel::JSON::XS 2.3310 + ExtUtils::MakeMaker 0 + JSON::PP 2.27300 + Scalar::Util 0 + perl 5.006 + JSON-Validator-5.15 + pathname: J/JH/JHTHORSEN/JSON-Validator-5.15.tar.gz + provides: + JSON::Validator 5.15 + JSON::Validator::Error undef + JSON::Validator::Formats undef + JSON::Validator::Joi undef + JSON::Validator::Schema undef + JSON::Validator::Schema::Draft201909 undef + JSON::Validator::Schema::Draft4 undef + JSON::Validator::Schema::Draft6 undef + JSON::Validator::Schema::Draft7 undef + JSON::Validator::Schema::OpenAPIv2 undef + JSON::Validator::Schema::OpenAPIv3 undef + JSON::Validator::Store undef + JSON::Validator::URI undef + JSON::Validator::Util undef + requirements: + ExtUtils::MakeMaker 0 + List::Util 1.45 + Mojolicious 7.28 + YAML::XS 0.67 + perl 5.016 + JSON-XS-4.03 + pathname: M/ML/MLEHMANN/JSON-XS-4.03.tar.gz + provides: + JSON::XS 4.03 + requirements: + Canary::Stability 0 + ExtUtils::MakeMaker 6.52 + Types::Serialiser 0 + common::sense 0 + LWP-MediaTypes-6.04 + pathname: O/OA/OALDERS/LWP-MediaTypes-6.04.tar.gz + provides: + LWP::MediaTypes 6.04 + requirements: + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 0 + File::Basename 0 + Scalar::Util 0 + perl 5.006002 + strict 0 + LWP-Protocol-https-6.14 + pathname: O/OA/OALDERS/LWP-Protocol-https-6.14.tar.gz + provides: + LWP::Protocol::https 6.14 + LWP::Protocol::https::Socket 6.14 + requirements: + ExtUtils::MakeMaker 0 + IO::Socket::SSL 1.970 + LWP::Protocol::http 0 + LWP::UserAgent 6.06 + Net::HTTPS 6 + base 0 + perl 5.008001 + strict 0 + Lingua-EN-Inflect-1.905 + pathname: D/DC/DCONWAY/Lingua-EN-Inflect-1.905.tar.gz + provides: + Lingua::EN::Inflect 1.905 + requirements: + ExtUtils::MakeMaker 0 + Test::More 0 + List-Compare-0.55 + pathname: J/JK/JKEENAN/List-Compare-0.55.tar.gz + provides: + List::Compare 0.55 + List::Compare::Accelerated 0.55 + List::Compare::Base::_Auxiliary 0.55 + List::Compare::Base::_Engine 0.55 + List::Compare::Functional 0.55 + List::Compare::Multiple 0.55 + List::Compare::Multiple::Accelerated 0.55 + requirements: + ExtUtils::MakeMaker 0 + List-MoreUtils-0.430 + pathname: R/RE/REHSACK/List-MoreUtils-0.430.tar.gz + provides: + List::MoreUtils 0.430 + List::MoreUtils::PP 0.430 + requirements: + Exporter::Tiny 0.038 + ExtUtils::MakeMaker 0 + List::MoreUtils::XS 0.430 + List-MoreUtils-XS-0.430 + pathname: R/RE/REHSACK/List-MoreUtils-XS-0.430.tar.gz + provides: + List::MoreUtils::XS 0.430 + requirements: + Carp 0 + ExtUtils::MakeMaker 0 + File::Basename 0 + File::Copy 0 + File::Path 0 + File::Spec 0 + IPC::Cmd 0 + XSLoader 0.22 + base 0 + List-SomeUtils-0.59 + pathname: D/DR/DROLSKY/List-SomeUtils-0.59.tar.gz + provides: + List::SomeUtils 0.59 + List::SomeUtils::PP 0.59 + requirements: + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 0 + List::SomeUtils::XS 0.54 + List::Util 0 + Module::Implementation 0.04 + Text::ParseWords 0 + perl 5.006 + strict 0 + vars 0 + warnings 0 + List-SomeUtils-XS-0.58 + pathname: D/DR/DROLSKY/List-SomeUtils-XS-0.58.tar.gz + provides: + List::SomeUtils::XS 0.58 + requirements: + ExtUtils::MakeMaker 0 + XSLoader 0 + strict 0 + warnings 0 + Log-Any-1.717 + pathname: P/PR/PREACTION/Log-Any-1.717.tar.gz + provides: + Log::Any 1.717 + Log::Any::Adapter 1.717 + Log::Any::Adapter::Base 1.717 + Log::Any::Adapter::Capture 1.717 + Log::Any::Adapter::File 1.717 + Log::Any::Adapter::Multiplex 1.717 + Log::Any::Adapter::Null 1.717 + Log::Any::Adapter::Stderr 1.717 + Log::Any::Adapter::Stdout 1.717 + Log::Any::Adapter::Syslog 1.717 + Log::Any::Adapter::Test 1.717 + Log::Any::Adapter::Util 1.717 + Log::Any::Manager 1.717 + Log::Any::Proxy 1.717 + Log::Any::Proxy::Null 1.717 + Log::Any::Proxy::Test 1.717 + Log::Any::Proxy::WithStackTrace 1.717 + Log::Any::Test 1.717 + requirements: + ExtUtils::MakeMaker 0 + Log-Any-Adapter-Log4perl-0.09 + pathname: P/PR/PREACTION/Log-Any-Adapter-Log4perl-0.09.tar.gz + provides: + Log::Any::Adapter::Log4perl 0.09 + requirements: + ExtUtils::MakeMaker 6.17 + Log::Any::Adapter::Base 0 + Log::Any::Adapter::Util 1.03 + Log::Log4perl 1.32 + base 0 + perl 5.006 + strict 0 + warnings 0 + Log-Contextual-0.009001 + pathname: H/HA/HAARG/Log-Contextual-0.009001.tar.gz + provides: + Log::Contextual 0.009001 + Log::Contextual::Easy::Default 0.009001 + Log::Contextual::Easy::Package 0.009001 + Log::Contextual::Role::Router 0.009001 + Log::Contextual::Role::Router::HasLogger 0.009001 + Log::Contextual::Role::Router::SetLogger 0.009001 + Log::Contextual::Role::Router::WithLogger 0.009001 + Log::Contextual::Router 0.009001 + Log::Contextual::SimpleLogger 0.009001 + Log::Contextual::TeeLogger 0.009001 + Log::Contextual::WarnLogger 0.009001 + requirements: + Carp 0 + Data::Dumper::Concise 0 + ExtUtils::MakeMaker 0 + Moo 1.003000 + Scalar::Util 0 + perl 5.008001 + Log-Dispatch-2.71 + pathname: D/DR/DROLSKY/Log-Dispatch-2.71.tar.gz + provides: + Log::Dispatch 2.71 + Log::Dispatch::ApacheLog 2.71 + Log::Dispatch::Base 2.71 + Log::Dispatch::Code 2.71 + Log::Dispatch::Email 2.71 + Log::Dispatch::Email::MIMELite 2.71 + Log::Dispatch::Email::MailSend 2.71 + Log::Dispatch::Email::MailSender 2.71 + Log::Dispatch::Email::MailSendmail 2.71 + Log::Dispatch::File 2.71 + Log::Dispatch::File::Locked 2.71 + Log::Dispatch::Handle 2.71 + Log::Dispatch::Null 2.71 + Log::Dispatch::Output 2.71 + Log::Dispatch::Screen 2.71 + Log::Dispatch::Syslog 2.71 + Log::Dispatch::Types 2.71 + Log::Dispatch::Vars 2.71 + requirements: + Carp 0 + Devel::GlobalDestruction 0 + Dist::CheckConflicts 0.02 + Encode 0 + Exporter 0 + ExtUtils::MakeMaker 0 + Fcntl 0 + IO::Handle 0 + Module::Runtime 0 + Params::ValidationCompiler 0 + Scalar::Util 0 + Specio 0.32 + Specio::Declare 0 + Specio::Exporter 0 + Specio::Library::Builtins 0 + Specio::Library::Numeric 0 + Specio::Library::String 0 + Sys::Syslog 0.28 + Try::Tiny 0 + base 0 + namespace::autoclean 0 + parent 0 + perl 5.006 + strict 0 + warnings 0 + Log-Log4perl-1.57 + pathname: E/ET/ETJ/Log-Log4perl-1.57.tar.gz + provides: + L4pResurrectable 0.01 + Log::Log4perl 1.57 + Log::Log4perl::Appender undef + Log::Log4perl::Appender::Buffer 1.53 + Log::Log4perl::Appender::DBI undef + Log::Log4perl::Appender::File undef + Log::Log4perl::Appender::Limit 1.53 + Log::Log4perl::Appender::RRDs undef + Log::Log4perl::Appender::Screen undef + Log::Log4perl::Appender::ScreenColoredLevels undef + Log::Log4perl::Appender::Socket undef + Log::Log4perl::Appender::String undef + Log::Log4perl::Appender::Synchronized 1.53 + Log::Log4perl::Appender::TestArrayBuffer undef + Log::Log4perl::Appender::TestBuffer undef + Log::Log4perl::Appender::TestFileCreeper undef + Log::Log4perl::Catalyst 1.53 + Log::Log4perl::Config undef + Log::Log4perl::Config::BaseConfigurator undef + Log::Log4perl::Config::DOMConfigurator 0.03 + Log::Log4perl::Config::PropertyConfigurator undef + Log::Log4perl::Config::Watch undef + Log::Log4perl::DateFormat undef + Log::Log4perl::Filter undef + Log::Log4perl::Filter::Boolean undef + Log::Log4perl::Filter::LevelMatch undef + Log::Log4perl::Filter::LevelRange undef + Log::Log4perl::Filter::MDC undef + Log::Log4perl::Filter::StringMatch undef + Log::Log4perl::InternalDebug undef + Log::Log4perl::JavaMap undef + Log::Log4perl::JavaMap::ConsoleAppender undef + Log::Log4perl::JavaMap::FileAppender undef + Log::Log4perl::JavaMap::JDBCAppender undef + Log::Log4perl::JavaMap::NTEventLogAppender undef + Log::Log4perl::JavaMap::RollingFileAppender undef + Log::Log4perl::JavaMap::SyslogAppender undef + Log::Log4perl::JavaMap::TestBuffer undef + Log::Log4perl::Layout undef + Log::Log4perl::Layout::NoopLayout undef + Log::Log4perl::Layout::PatternLayout undef + Log::Log4perl::Layout::PatternLayout::Multiline undef + Log::Log4perl::Layout::SimpleLayout undef + Log::Log4perl::Level undef + Log::Log4perl::Logger undef + Log::Log4perl::MDC undef + Log::Log4perl::NDC undef + Log::Log4perl::Resurrector undef + Log::Log4perl::Util undef + Log::Log4perl::Util::Semaphore undef + Log::Log4perl::Util::TimeTracker undef + requirements: + ExtUtils::MakeMaker 0 + File::Path 2.07 + File::Spec 0.82 + perl 5.006 + Log-Log4perl-Layout-JSON-0.61 + pathname: M/MS/MSCHOUT/Log-Log4perl-Layout-JSON-0.61.tar.gz + provides: + Log::Log4perl::Layout::JSON 0.61 + requirements: + Carp 0 + Class::Tiny 0 + ExtUtils::MakeMaker 0 + JSON::MaybeXS 0 + Log::Log4perl 0 + Log::Log4perl::Layout 0 + Log::Log4perl::Layout::PatternLayout 0 + Log::Log4perl::Level 0 + Scalar::Util 0 + parent 0 + perl 5.010 + strict 0 + warnings 0 + MCE-1.901 + pathname: M/MA/MARIOROY/MCE-1.901.tar.gz + provides: + MCE 1.901 + MCE::Candy 1.901 + MCE::Channel 1.901 + MCE::Channel::Mutex 1.901 + MCE::Channel::MutexFast 1.901 + MCE::Channel::Simple 1.901 + MCE::Channel::SimpleFast 1.901 + MCE::Channel::Threads 1.901 + MCE::Channel::ThreadsFast 1.901 + MCE::Child 1.901 + MCE::Core 1.901 + MCE::Core::Input::Generator 1.901 + MCE::Core::Input::Handle 1.901 + MCE::Core::Input::Iterator 1.901 + MCE::Core::Input::Request 1.901 + MCE::Core::Input::Sequence 1.901 + MCE::Core::Manager 1.901 + MCE::Core::Validation 1.901 + MCE::Core::Worker 1.901 + MCE::Flow 1.901 + MCE::Grep 1.901 + MCE::Loop 1.901 + MCE::Map 1.901 + MCE::Mutex 1.901 + MCE::Mutex::Channel 1.901 + MCE::Mutex::Channel2 1.901 + MCE::Mutex::Flock 1.901 + MCE::Queue 1.901 + MCE::Relay 1.901 + MCE::Signal 1.901 + MCE::Step 1.901 + MCE::Stream 1.901 + MCE::Subs 1.901 + MCE::Util 1.901 + requirements: + Carp 0 + Errno 0 + ExtUtils::MakeMaker 0 + Fcntl 0 + File::Path 0 + Getopt::Long 0 + IO::Handle 0 + Scalar::Util 0 + Socket 0 + Storable 2.04 + Time::HiRes 0 + base 0 + bytes 0 + constant 0 + open 0 + perl 5.008001 + strict 0 + warnings 0 + MIME-Base32-1.303 + pathname: R/RE/REHSACK/MIME-Base32-1.303.tar.gz + provides: + MIME::Base32 1.303 + requirements: + Exporter 0 + ExtUtils::MakeMaker 0 + perl 5.008001 + utf8 0 + MIME-Charset-1.013.1 + pathname: N/NE/NEZUMI/MIME-Charset-1.013.1.tar.gz + provides: + MIME::Charset v1.13.1 + requirements: + CPAN 0 + Encode 1.98 + ExtUtils::MakeMaker 6.42 + Test::More 0 + perl 5.005 + MIME-Types-2.28 + pathname: M/MA/MARKOV/MIME-Types-2.28.tar.gz + provides: + MIME::Type 2.28 + MIME::Types 2.28 + MojoX::MIME::Types 2.28 + requirements: + ExtUtils::MakeMaker 0 + File::Basename 0 + File::Spec 0 + List::Util 0 + Test::More 0.47 + MRO-Compat-0.15 + pathname: H/HA/HAARG/MRO-Compat-0.15.tar.gz + provides: + MRO::Compat 0.15 + requirements: + ExtUtils::MakeMaker 0 + perl 5.006 + MailTools-2.22 + pathname: M/MA/MARKOV/MailTools-2.22.tar.gz + provides: + Mail::Address 2.22 + Mail::Cap 2.22 + Mail::Field 2.22 + Mail::Field::AddrList 2.22 + Mail::Field::Date 2.22 + Mail::Field::Generic 2.22 + Mail::Filter 2.22 + Mail::Header 2.22 + Mail::Internet 2.22 + Mail::Mailer 2.22 + Mail::Mailer::qmail 2.22 + Mail::Mailer::rfc822 2.22 + Mail::Mailer::sendmail 2.22 + Mail::Mailer::smtp 2.22 + Mail::Mailer::smtp::pipe 2.22 + Mail::Mailer::smtps 2.22 + Mail::Mailer::smtps::pipe 2.22 + Mail::Mailer::testfile 2.22 + Mail::Mailer::testfile::pipe 2.22 + Mail::Send 2.22 + Mail::Util 2.22 + MailTools 2.22 + requirements: + Date::Format 0 + Date::Parse 0 + ExtUtils::MakeMaker 0 + IO::Handle 0 + Net::Domain 1.05 + Net::SMTP 1.28 + Test::More 0 + Math-BigInt-2.005003 + pathname: P/PJ/PJACKLAM/Math-BigInt-2.005003.tar.gz + provides: + Math::BigFloat 2.005003 + Math::BigInt 2.005003 + Math::BigInt::Calc 2.005003 + Math::BigInt::Lib 2.005003 + Math::BigRat 2.005003 + requirements: + Carp 1.22 + ExtUtils::MakeMaker 6.58 + Math::Complex 1.36 + Scalar::Util 0 + perl 5.006001 + MetaCPAN-Client-2.033000 + pathname: M/MI/MICKEY/MetaCPAN-Client-2.033000.tar.gz + provides: + MetaCPAN::Client 2.033000 + MetaCPAN::Client::Author 2.033000 + MetaCPAN::Client::Cover 2.033000 + MetaCPAN::Client::Distribution 2.033000 + MetaCPAN::Client::DownloadURL 2.033000 + MetaCPAN::Client::Favorite 2.033000 + MetaCPAN::Client::File 2.033000 + MetaCPAN::Client::Mirror 2.033000 + MetaCPAN::Client::Module 2.033000 + MetaCPAN::Client::Package 2.033000 + MetaCPAN::Client::Permission 2.033000 + MetaCPAN::Client::Pod 2.033000 + MetaCPAN::Client::Rating 2.033000 + MetaCPAN::Client::Release 2.033000 + MetaCPAN::Client::Request 2.033000 + MetaCPAN::Client::ResultSet 2.033000 + MetaCPAN::Client::Role::Entity 2.033000 + MetaCPAN::Client::Role::HasUA 2.033000 + MetaCPAN::Client::Scroll 2.033000 + MetaCPAN::Client::Types 2.033000 + requirements: + Carp 0 + ExtUtils::MakeMaker 7.1101 + HTTP::Tiny 0.056 + IO::Socket::SSL 1.42 + JSON::MaybeXS 0 + JSON::PP 0 + Moo 0 + Moo::Role 0 + Net::SSLeay 1.49 + Ref::Util 0 + Safe::Isa 0 + Type::Tiny 0 + URI::Escape 0 + perl 5.010 + strict 0 + warnings 0 + MetaCPAN-Moose-0.000003 + pathname: O/OA/OALDERS/MetaCPAN-Moose-0.000003.tar.gz + provides: + MetaCPAN::Moose 0.000003 + requirements: + ExtUtils::MakeMaker 0 + Import::Into 1.002005 + Moose 2.1605 + MooseX::StrictConstructor 0.19 + namespace::autoclean 0.28 + perl 5.006 + strict 0 + warnings 0 + MetaCPAN-Pod-HTML-0.004000 + pathname: H/HA/HAARG/MetaCPAN-Pod-HTML-0.004000.tar.gz + provides: + MetaCPAN::Pod::HTML 0.004000 + MetaCPAN::Pod::XHTML 0.004000 + Pod::Simple::Role::StripVerbatimIndent 0.004000 + Pod::Simple::Role::WithHighlightConfig 0.004000 + Pod::Simple::Role::XHTML::HTML5 0.004000 + Pod::Simple::Role::XHTML::RepairLinkEncoding 0.004000 + Pod::Simple::Role::XHTML::WithAccurateTargets 0.004000 + Pod::Simple::Role::XHTML::WithErrata 0.004000 + Pod::Simple::Role::XHTML::WithExtraTargets 0.004000 + Pod::Simple::Role::XHTML::WithHighlightConfig 0.004000 + Pod::Simple::Role::XHTML::WithLinkMappings 0.004000 + Pod::Simple::Role::XHTML::WithPostProcess 0.004000 + requirements: + ExtUtils::MakeMaker 0 + HTML::Entities 3.69 + Moo 2.003000 + Moo::Role 2.003000 + Pod::Simple::XHTML 3.45 + URL::Encode 0.03 + namespace::clean 0.27 + MetaCPAN-Role-1.00 + pathname: L/LL/LLAP/MetaCPAN-Role-1.00.tar.gz + provides: + MetaCPAN::Role 1.00 + MetaCPAN::Role::Fastly 1.00 + MetaCPAN::Role::Fastly::Catalyst 1.00 + requirements: + Carp 0 + CatalystX::Fastly::Role::Response 0.07 + ExtUtils::MakeMaker 0 + Moose::Role 0 + MooseX::Fastly::Role 0.04 + Net::Fastly 1.05 + Minion-10.31 + pathname: S/SR/SRI/Minion-10.31.tar.gz + provides: + LinkCheck undef + LinkCheck::Controller::Links undef + LinkCheck::Task::CheckLinks undef + Minion 10.31 + Minion::Backend undef + Minion::Backend::Pg undef + Minion::Command::minion undef + Minion::Command::minion::job undef + Minion::Command::minion::worker undef + Minion::Iterator undef + Minion::Job undef + Minion::Worker undef + Mojolicious::Plugin::Minion undef + Mojolicious::Plugin::Minion::Admin undef + requirements: + ExtUtils::MakeMaker 0 + Mojolicious 9.0 + YAML::XS 0.67 + perl 5.016 + Minion-Backend-SQLite-v5.0.7 + pathname: D/DB/DBOOK/Minion-Backend-SQLite-v5.0.7.tar.gz + provides: + Minion::Backend::SQLite v5.0.7 + requirements: + List::Util 0 + Minion 10.13 + Module::Build::Tiny 0.034 + Mojo::SQLite 3.000 + Mojolicious 7.49 + Sys::Hostname 0 + Time::HiRes 0 + perl 5.010001 + Mixin-Linewise-0.111 + pathname: R/RJ/RJBS/Mixin-Linewise-0.111.tar.gz + provides: + Mixin::Linewise 0.111 + Mixin::Linewise::Readers 0.111 + Mixin::Linewise::Writers 0.111 + requirements: + Carp 0 + ExtUtils::MakeMaker 6.78 + IO::File 0 + PerlIO::utf8_strict 0 + Sub::Exporter 0 + perl 5.012 + strict 0 + warnings 0 + Module-Build-0.4234 + pathname: L/LE/LEONT/Module-Build-0.4234.tar.gz + provides: + Module::Build 0.4234 + Module::Build::Base 0.4234 + Module::Build::Compat 0.4234 + Module::Build::Config 0.4234 + Module::Build::Cookbook 0.4234 + Module::Build::Dumper 0.4234 + Module::Build::Notes 0.4234 + Module::Build::PPMMaker 0.4234 + Module::Build::Platform::Default 0.4234 + Module::Build::Platform::MacOS 0.4234 + Module::Build::Platform::Unix 0.4234 + Module::Build::Platform::VMS 0.4234 + Module::Build::Platform::VOS 0.4234 + Module::Build::Platform::Windows 0.4234 + Module::Build::Platform::aix 0.4234 + Module::Build::Platform::cygwin 0.4234 + Module::Build::Platform::darwin 0.4234 + Module::Build::Platform::os2 0.4234 + Module::Build::PodParser 0.4234 + requirements: + CPAN::Meta 2.142060 + Cwd 0 + Data::Dumper 0 + ExtUtils::CBuilder 0.27 + ExtUtils::Install 0 + ExtUtils::Manifest 0 + ExtUtils::Mkbootstrap 0 + ExtUtils::ParseXS 2.21 + File::Basename 0 + File::Compare 0 + File::Copy 0 + File::Find 0 + File::Path 0 + File::Spec 0.82 + Getopt::Long 0 + Module::Metadata 1.000002 + Perl::OSType 1 + TAP::Harness 3.29 + Text::Abbrev 0 + Text::ParseWords 0 + perl 5.006001 + version 0.87 + Module-Build-Tiny-0.051 + pathname: L/LE/LEONT/Module-Build-Tiny-0.051.tar.gz + provides: + Module::Build::Tiny 0.051 + requirements: + CPAN::Meta 0 + DynaLoader 0 + Exporter 5.57 + ExtUtils::CBuilder 0 + ExtUtils::Config 0.003 + ExtUtils::Helpers 0.020 + ExtUtils::Install 0 + ExtUtils::InstallPaths 0.002 + ExtUtils::ParseXS 0 + File::Basename 0 + File::Find 0 + File::Path 0 + File::Spec::Functions 0 + Getopt::Long 2.36 + JSON::PP 2 + Pod::Man 0 + TAP::Harness::Env 0 + perl 5.006 + strict 0 + warnings 0 + Module-CPANfile-1.1004 + pathname: M/MI/MIYAGAWA/Module-CPANfile-1.1004.tar.gz + provides: + Module::CPANfile 1.1004 + Module::CPANfile::Environment undef + Module::CPANfile::Prereq undef + Module::CPANfile::Prereqs undef + Module::CPANfile::Requirement undef + requirements: + CPAN::Meta 2.12091 + CPAN::Meta::Prereqs 2.12091 + ExtUtils::MakeMaker 0 + parent 0 + Module-Faker-0.017 + pathname: R/RJ/RJBS/Module-Faker-0.017.tar.gz + provides: + Module::Faker 0.017 + Module::Faker::Appendix 0.017 + Module::Faker::Dist 0.017 + Module::Faker::File 0.017 + Module::Faker::Heavy 0.017 + Module::Faker::Module 0.017 + Module::Faker::Package 0.017 + requirements: + Archive::Any::Create 0 + CPAN::DistnameInfo 0 + CPAN::Meta 2.130880 + CPAN::Meta::Requirements 0 + Carp 0 + Encode 0 + ExtUtils::MakeMaker 0 + File::Next 0 + File::Path 0 + File::Temp 0 + Moose 0.33 + Moose::Role 0 + Moose::Util::TypeConstraints 0 + Parse::CPAN::Meta 1.4401 + Path::Class 0.06 + Text::Template 0 + strict 0 + warnings 0 + Module-Find-0.17 + pathname: C/CR/CRENZ/Module-Find-0.17.tar.gz + provides: + Module::Find 0.17 + requirements: + ExtUtils::MakeMaker 0 + File::Find 0 + File::Spec 0 + Test::More 0 + perl 5.008001 + Module-Implementation-0.09 + pathname: D/DR/DROLSKY/Module-Implementation-0.09.tar.gz + provides: + Module::Implementation 0.09 + requirements: + Carp 0 + ExtUtils::MakeMaker 0 + Module::Runtime 0.012 + Try::Tiny 0 + strict 0 + warnings 0 + Module-Load-Conditional-0.74 + pathname: B/BI/BINGOS/Module-Load-Conditional-0.74.tar.gz + provides: + Module::Load::Conditional 0.74 + requirements: + ExtUtils::MakeMaker 0 + Locale::Maketext::Simple 0 + Module::CoreList 2.22 + Module::Load 0.28 + Module::Metadata 1.000005 + Params::Check 0 + Test::More 0 + version 0.69 + Module-Metadata-1.000038 + pathname: E/ET/ETHER/Module-Metadata-1.000038.tar.gz + provides: + Module::Metadata 1.000038 + requirements: + Carp 0 + Encode 0 + ExtUtils::MakeMaker 0 + Fcntl 0 + File::Find 0 + File::Spec 0 + perl 5.006 + strict 0 + version 0.87 + warnings 0 + Module-Pluggable-6.3 + pathname: S/SI/SIMONW/Module-Pluggable-6.3.tar.gz + provides: + Devel::InnerPackage 0.4 + Module::Pluggable 6.3 + Module::Pluggable::Object 5.2 + requirements: + Exporter 5.57 + ExtUtils::MakeMaker 0 + File::Basename 0 + File::Find 0 + File::Spec 3.00 + File::Spec::Functions 0 + Scalar::Util 0 + if 0 + perl 5.006 + strict 0 + Module-Runtime-0.018 + pathname: H/HA/HAARG/Module-Runtime-0.018.tar.gz + provides: + Module::Runtime 0.018 + requirements: + ExtUtils::MakeMaker 0 + perl 5.006000 + Module-Runtime-Conflicts-0.003 + pathname: E/ET/ETHER/Module-Runtime-Conflicts-0.003.tar.gz + provides: + Module::Runtime::Conflicts 0.003 + requirements: + Dist::CheckConflicts 0 + ExtUtils::MakeMaker 0 + Module::Runtime 0 + perl 5.006 + strict 0 + warnings 0 + Mojo-Pg-4.27 + pathname: S/SR/SRI/Mojo-Pg-4.27.tar.gz + provides: + Mojo::Pg 4.27 + Mojo::Pg::Database undef + Mojo::Pg::Migrations undef + Mojo::Pg::PubSub undef + Mojo::Pg::Results undef + Mojo::Pg::Transaction undef + requirements: + DBD::Pg 3.007004 + ExtUtils::MakeMaker 0 + Mojolicious 8.50 + SQL::Abstract::Pg 1.0 + perl 5.016 + Mojo-SQLite-3.009 + pathname: D/DB/DBOOK/Mojo-SQLite-3.009.tar.gz + provides: + Mojo::SQLite 3.009 + Mojo::SQLite::Database 3.009 + Mojo::SQLite::Migrations 3.009 + Mojo::SQLite::PubSub 3.009 + Mojo::SQLite::Results 3.009 + Mojo::SQLite::Transaction 3.009 + requirements: + Carp 0 + DBD::SQLite 1.68 + DBI 1.627 + File::Spec::Functions 0 + File::Temp 0 + Module::Build::Tiny 0.034 + Mojolicious 8.03 + SQL::Abstract::Pg 1.0 + Scalar::Util 0 + URI 1.69 + URI::db 0.15 + URI::file 4.21 + perl 5.010001 + Mojolicious-9.39 + pathname: S/SR/SRI/Mojolicious-9.39.tar.gz + provides: + Mojo undef + Mojo::Asset undef + Mojo::Asset::File undef + Mojo::Asset::Memory undef + Mojo::Base undef + Mojo::BaseUtil undef + Mojo::ByteStream undef + Mojo::Cache undef + Mojo::Collection undef + Mojo::Content undef + Mojo::Content::MultiPart undef + Mojo::Content::Single undef + Mojo::Cookie undef + Mojo::Cookie::Request undef + Mojo::Cookie::Response undef + Mojo::DOM undef + Mojo::DOM::CSS undef + Mojo::DOM::HTML undef + Mojo::Date undef + Mojo::DynamicMethods undef + Mojo::EventEmitter undef + Mojo::Exception undef + Mojo::File undef + Mojo::Headers undef + Mojo::HelloWorld undef + Mojo::Home undef + Mojo::IOLoop undef + Mojo::IOLoop::Client undef + Mojo::IOLoop::Server undef + Mojo::IOLoop::Stream undef + Mojo::IOLoop::Subprocess undef + Mojo::IOLoop::TLS undef + Mojo::JSON undef + Mojo::JSON::Pointer undef + Mojo::Loader undef + Mojo::Log undef + Mojo::Message undef + Mojo::Message::Request undef + Mojo::Message::Response undef + Mojo::Parameters undef + Mojo::Path undef + Mojo::Promise undef + Mojo::Reactor undef + Mojo::Reactor::EV undef + Mojo::Reactor::Poll undef + Mojo::Server undef + Mojo::Server::CGI undef + Mojo::Server::Daemon undef + Mojo::Server::Hypnotoad undef + Mojo::Server::Morbo undef + Mojo::Server::Morbo::Backend undef + Mojo::Server::Morbo::Backend::Poll undef + Mojo::Server::PSGI undef + Mojo::Server::Prefork undef + Mojo::Template undef + Mojo::Transaction undef + Mojo::Transaction::HTTP undef + Mojo::Transaction::WebSocket undef + Mojo::URL undef + Mojo::Upload undef + Mojo::UserAgent undef + Mojo::UserAgent::CookieJar undef + Mojo::UserAgent::Proxy undef + Mojo::UserAgent::Server undef + Mojo::UserAgent::Transactor undef + Mojo::Util undef + Mojo::WebSocket undef + Mojolicious 9.39 + Mojolicious::Command undef + Mojolicious::Command::Author::cpanify undef + Mojolicious::Command::Author::generate undef + Mojolicious::Command::Author::generate::app undef + Mojolicious::Command::Author::generate::dockerfile undef + Mojolicious::Command::Author::generate::lite_app undef + Mojolicious::Command::Author::generate::makefile undef + Mojolicious::Command::Author::generate::plugin undef + Mojolicious::Command::Author::inflate undef + Mojolicious::Command::cgi undef + Mojolicious::Command::daemon undef + Mojolicious::Command::eval undef + Mojolicious::Command::get undef + Mojolicious::Command::prefork undef + Mojolicious::Command::psgi undef + Mojolicious::Command::routes undef + Mojolicious::Command::version undef + Mojolicious::Commands undef + Mojolicious::Controller undef + Mojolicious::Lite undef + Mojolicious::Plugin undef + Mojolicious::Plugin::Config undef + Mojolicious::Plugin::DefaultHelpers undef + Mojolicious::Plugin::EPLRenderer undef + Mojolicious::Plugin::EPRenderer undef + Mojolicious::Plugin::HeaderCondition undef + Mojolicious::Plugin::JSONConfig undef + Mojolicious::Plugin::Mount undef + Mojolicious::Plugin::NotYAMLConfig undef + Mojolicious::Plugin::TagHelpers undef + Mojolicious::Plugins undef + Mojolicious::Renderer undef + Mojolicious::Routes undef + Mojolicious::Routes::Match undef + Mojolicious::Routes::Pattern undef + Mojolicious::Routes::Route undef + Mojolicious::Sessions undef + Mojolicious::Static undef + Mojolicious::Types undef + Mojolicious::Validator undef + Mojolicious::Validator::Validation undef + Test::Mojo undef + ojo undef + requirements: + ExtUtils::MakeMaker 0 + IO::Socket::IP 0.37 + Sub::Util 1.41 + perl 5.016 + Mojolicious-Plugin-MountPSGI-0.15 + pathname: J/JB/JBERGER/Mojolicious-Plugin-MountPSGI-0.15.tar.gz + provides: + Mojolicious::Plugin::MountPSGI 0.15 + Mojolicious::Plugin::MountPSGI::Proxy undef + requirements: + ExtUtils::MakeMaker 0 + Mojolicious 7.70 + Plack 0 + Mojolicious-Plugin-OpenAPI-5.11 + pathname: J/JH/JHTHORSEN/Mojolicious-Plugin-OpenAPI-5.11.tar.gz + provides: + Mojolicious::Plugin::OpenAPI 5.11 + Mojolicious::Plugin::OpenAPI::Cors undef + Mojolicious::Plugin::OpenAPI::Parameters undef + Mojolicious::Plugin::OpenAPI::Security undef + Mojolicious::Plugin::OpenAPI::SpecRenderer undef + requirements: + ExtUtils::MakeMaker 0 + JSON::Validator 5.13 + Mojolicious 9.00 + perl 5.016 + Mojolicious-Plugin-Web-Auth-0.17 + pathname: H/HA/HAYAJO/Mojolicious-Plugin-Web-Auth-0.17.tar.gz + provides: + Mojolicious::Plugin::Web::Auth 0.17 + Mojolicious::Plugin::Web::Auth::Base undef + Mojolicious::Plugin::Web::Auth::OAuth undef + Mojolicious::Plugin::Web::Auth::OAuth2 undef + Mojolicious::Plugin::Web::Auth::Site::Dropbox undef + Mojolicious::Plugin::Web::Auth::Site::Facebook undef + Mojolicious::Plugin::Web::Auth::Site::Github undef + Mojolicious::Plugin::Web::Auth::Site::Google undef + Mojolicious::Plugin::Web::Auth::Site::Instagram undef + Mojolicious::Plugin::Web::Auth::Site::Twitter undef + Mojolicious::Plugin::Web::Auth::Site::Yandex undef + requirements: + IO::Socket::SSL 1.77 + Module::Build::Tiny 0.035 + Mojolicious 7.13 + Net::OAuth 0.28 + perl 5.010001 + Moo-2.005005 + pathname: H/HA/HAARG/Moo-2.005005.tar.gz + provides: + Method::Generate::Accessor undef + Method::Generate::BuildAll undef + Method::Generate::Constructor undef + Method::Generate::DemolishAll undef + Moo 2.005005 + Moo::HandleMoose undef + Moo::HandleMoose::FakeConstructor undef + Moo::HandleMoose::FakeMetaClass undef + Moo::HandleMoose::_TypeMap undef + Moo::Object undef + Moo::Role 2.005005 + Moo::_Utils undef + Moo::sification undef + oo undef + requirements: + Carp 0 + Class::Method::Modifiers 1.10 + Exporter 0 + ExtUtils::MakeMaker 0 + Role::Tiny 2.002003 + Scalar::Util 1.00 + Sub::Defer 2.006006 + Sub::Quote 2.006006 + perl 5.006 + MooX-Aliases-0.001006 + pathname: H/HA/HAARG/MooX-Aliases-0.001006.tar.gz + provides: + MooX::Aliases 0.001006 + requirements: + Class::Method::Modifiers 1.05 + Moo 1.001 + perl 5.006 + strictures 1 + MooX-Locale-Passthrough-0.001 + pathname: R/RE/REHSACK/MooX-Locale-Passthrough-0.001.tar.gz + provides: + MooX::Locale::Passthrough 0.001 + requirements: + ExtUtils::MakeMaker 0 + Moo 1.003 + perl 5.008001 + MooX-Options-4.103 + pathname: R/RE/REHSACK/MooX-Options-4.103.tar.gz + provides: + MooX::Options 4.103 + MooX::Options::Descriptive 4.103 + MooX::Options::Descriptive::Usage 4.103 + MooX::Options::Role 4.103 + requirements: + ExtUtils::MakeMaker 0 + Getopt::Long 2.43 + Getopt::Long::Descriptive 0.099 + MRO::Compat 0 + Module::Runtime 0 + Moo 1.003 + MooX::Locale::Passthrough 0 + Path::Class 0.32 + Pod::Usage 0 + Text::LineFold 0 + perl 5.008001 + strictures 2 + MooX-StrictConstructor-0.013 + pathname: H/HA/HAARG/MooX-StrictConstructor-0.013.tar.gz + provides: + MooX::StrictConstructor 0.013 + MooX::StrictConstructor::Role::BuildAll 0.013 + MooX::StrictConstructor::Role::Constructor 0.013 + MooX::StrictConstructor::Role::Constructor::Base 0.013 + MooX::StrictConstructor::Role::Constructor::Late 0.013 + requirements: + ExtUtils::MakeMaker 0 + Moo 2.004000 + Moo::Role 0 + perl 5.008 + MooX-Traits-0.005 + pathname: T/TO/TOBYINK/MooX-Traits-0.005.tar.gz + provides: + MooX::Traits 0.005 + MooX::Traits::Util 0.005 + requirements: + Exporter::Shiny 0 + ExtUtils::MakeMaker 6.17 + Module::Runtime 0 + Role::Tiny 1.000000 + Scalar::Util 0 + perl 5.006000 + MooX-Types-MooseLike-0.29 + pathname: M/MA/MATEU/MooX-Types-MooseLike-0.29.tar.gz + provides: + MooX::Types::MooseLike 0.29 + MooX::Types::MooseLike::Base 0.29 + requirements: + ExtUtils::MakeMaker 0 + Module::Runtime 0.014 + MooX-Types-MooseLike-Numeric-1.03 + pathname: M/MA/MATEU/MooX-Types-MooseLike-Numeric-1.03.tar.gz + provides: + MooX::Types::MooseLike::Numeric 1.03 + requirements: + ExtUtils::MakeMaker 0 + Moo 1.004002 + MooX::Types::MooseLike 0.23 + Test::Fatal 0.003 + Test::More 0.96 + Moose-2.2207 + pathname: E/ET/ETHER/Moose-2.2207.tar.gz + provides: + Class::MOP 2.2207 + Class::MOP::Attribute 2.2207 + Class::MOP::Class 2.2207 + Class::MOP::Class::Immutable::Trait 2.2207 + Class::MOP::Deprecated 2.2207 + Class::MOP::Instance 2.2207 + Class::MOP::Method 2.2207 + Class::MOP::Method::Accessor 2.2207 + Class::MOP::Method::Constructor 2.2207 + Class::MOP::Method::Generated 2.2207 + Class::MOP::Method::Inlined 2.2207 + Class::MOP::Method::Meta 2.2207 + Class::MOP::Method::Wrapped 2.2207 + Class::MOP::MiniTrait 2.2207 + Class::MOP::Mixin 2.2207 + Class::MOP::Mixin::AttributeCore 2.2207 + Class::MOP::Mixin::HasAttributes 2.2207 + Class::MOP::Mixin::HasMethods 2.2207 + Class::MOP::Mixin::HasOverloads 2.2207 + Class::MOP::Module 2.2207 + Class::MOP::Object 2.2207 + Class::MOP::Overload 2.2207 + Class::MOP::Package 2.2207 + Moose 2.2207 + Moose::Cookbook 2.2207 + Moose::Cookbook::Basics::BankAccount_MethodModifiersAndSubclassing 2.2207 + Moose::Cookbook::Basics::BinaryTree_AttributeFeatures 2.2207 + Moose::Cookbook::Basics::BinaryTree_BuilderAndLazyBuild 2.2207 + Moose::Cookbook::Basics::Company_Subtypes 2.2207 + Moose::Cookbook::Basics::DateTime_ExtendingNonMooseParent 2.2207 + Moose::Cookbook::Basics::Document_AugmentAndInner 2.2207 + Moose::Cookbook::Basics::Genome_OverloadingSubtypesAndCoercion 2.2207 + Moose::Cookbook::Basics::HTTP_SubtypesAndCoercion 2.2207 + Moose::Cookbook::Basics::Immutable 2.2207 + Moose::Cookbook::Basics::Person_BUILDARGSAndBUILD 2.2207 + Moose::Cookbook::Basics::Point_AttributesAndSubclassing 2.2207 + Moose::Cookbook::Extending::Debugging_BaseClassRole 2.2207 + Moose::Cookbook::Extending::ExtensionOverview 2.2207 + Moose::Cookbook::Extending::Mooseish_MooseSugar 2.2207 + Moose::Cookbook::Legacy::Debugging_BaseClassReplacement 2.2207 + Moose::Cookbook::Legacy::Labeled_AttributeMetaclass 2.2207 + Moose::Cookbook::Legacy::Table_ClassMetaclass 2.2207 + Moose::Cookbook::Meta::GlobRef_InstanceMetaclass 2.2207 + Moose::Cookbook::Meta::Labeled_AttributeTrait 2.2207 + Moose::Cookbook::Meta::PrivateOrPublic_MethodMetaclass 2.2207 + Moose::Cookbook::Meta::Table_MetaclassTrait 2.2207 + Moose::Cookbook::Meta::WhyMeta 2.2207 + Moose::Cookbook::Roles::ApplicationToInstance 2.2207 + Moose::Cookbook::Roles::Comparable_CodeReuse 2.2207 + Moose::Cookbook::Roles::Restartable_AdvancedComposition 2.2207 + Moose::Cookbook::Snack::Keywords 2.2207 + Moose::Cookbook::Snack::Types 2.2207 + Moose::Cookbook::Style 2.2207 + Moose::Deprecated 2.2207 + Moose::Exception 2.2207 + Moose::Exception::AccessorMustReadWrite 2.2207 + Moose::Exception::AddParameterizableTypeTakesParameterizableType 2.2207 + Moose::Exception::AddRoleTakesAMooseMetaRoleInstance 2.2207 + Moose::Exception::AddRoleToARoleTakesAMooseMetaRole 2.2207 + Moose::Exception::ApplyTakesABlessedInstance 2.2207 + Moose::Exception::AttachToClassNeedsAClassMOPClassInstanceOrASubclass 2.2207 + Moose::Exception::AttributeConflictInRoles 2.2207 + Moose::Exception::AttributeConflictInSummation 2.2207 + Moose::Exception::AttributeExtensionIsNotSupportedInRoles 2.2207 + Moose::Exception::AttributeIsRequired 2.2207 + Moose::Exception::AttributeMustBeAnClassMOPMixinAttributeCoreOrSubclass 2.2207 + Moose::Exception::AttributeNamesDoNotMatch 2.2207 + Moose::Exception::AttributeValueIsNotAnObject 2.2207 + Moose::Exception::AttributeValueIsNotDefined 2.2207 + Moose::Exception::AutoDeRefNeedsArrayRefOrHashRef 2.2207 + Moose::Exception::BadOptionFormat 2.2207 + Moose::Exception::BothBuilderAndDefaultAreNotAllowed 2.2207 + Moose::Exception::BuilderDoesNotExist 2.2207 + Moose::Exception::BuilderMethodNotSupportedForAttribute 2.2207 + Moose::Exception::BuilderMethodNotSupportedForInlineAttribute 2.2207 + Moose::Exception::BuilderMustBeAMethodName 2.2207 + Moose::Exception::CallingMethodOnAnImmutableInstance 2.2207 + Moose::Exception::CallingReadOnlyMethodOnAnImmutableInstance 2.2207 + Moose::Exception::CanExtendOnlyClasses 2.2207 + Moose::Exception::CanOnlyConsumeRole 2.2207 + Moose::Exception::CanOnlyWrapBlessedCode 2.2207 + Moose::Exception::CanReblessOnlyIntoASubclass 2.2207 + Moose::Exception::CanReblessOnlyIntoASuperclass 2.2207 + Moose::Exception::CannotAddAdditionalTypeCoercionsToUnion 2.2207 + Moose::Exception::CannotAddAsAnAttributeToARole 2.2207 + Moose::Exception::CannotApplyBaseClassRolesToRole 2.2207 + Moose::Exception::CannotAssignValueToReadOnlyAccessor 2.2207 + Moose::Exception::CannotAugmentIfLocalMethodPresent 2.2207 + Moose::Exception::CannotAugmentNoSuperMethod 2.2207 + Moose::Exception::CannotAutoDerefWithoutIsa 2.2207 + Moose::Exception::CannotAutoDereferenceTypeConstraint 2.2207 + Moose::Exception::CannotCalculateNativeType 2.2207 + Moose::Exception::CannotCallAnAbstractBaseMethod 2.2207 + Moose::Exception::CannotCallAnAbstractMethod 2.2207 + Moose::Exception::CannotCoerceAWeakRef 2.2207 + Moose::Exception::CannotCoerceAttributeWhichHasNoCoercion 2.2207 + Moose::Exception::CannotCreateHigherOrderTypeWithoutATypeParameter 2.2207 + Moose::Exception::CannotCreateMethodAliasLocalMethodIsPresent 2.2207 + Moose::Exception::CannotCreateMethodAliasLocalMethodIsPresentInClass 2.2207 + Moose::Exception::CannotDelegateLocalMethodIsPresent 2.2207 + Moose::Exception::CannotDelegateWithoutIsa 2.2207 + Moose::Exception::CannotFindDelegateMetaclass 2.2207 + Moose::Exception::CannotFindType 2.2207 + Moose::Exception::CannotFindTypeGivenToMatchOnType 2.2207 + Moose::Exception::CannotFixMetaclassCompatibility 2.2207 + Moose::Exception::CannotGenerateInlineConstraint 2.2207 + Moose::Exception::CannotInitializeMooseMetaRoleComposite 2.2207 + Moose::Exception::CannotInlineTypeConstraintCheck 2.2207 + Moose::Exception::CannotLocatePackageInINC 2.2207 + Moose::Exception::CannotMakeMetaclassCompatible 2.2207 + Moose::Exception::CannotOverrideALocalMethod 2.2207 + Moose::Exception::CannotOverrideBodyOfMetaMethods 2.2207 + Moose::Exception::CannotOverrideLocalMethodIsPresent 2.2207 + Moose::Exception::CannotOverrideNoSuperMethod 2.2207 + Moose::Exception::CannotRegisterUnnamedTypeConstraint 2.2207 + Moose::Exception::CannotUseLazyBuildAndDefaultSimultaneously 2.2207 + Moose::Exception::CircularReferenceInAlso 2.2207 + Moose::Exception::ClassDoesNotHaveInitMeta 2.2207 + Moose::Exception::ClassDoesTheExcludedRole 2.2207 + Moose::Exception::ClassNamesDoNotMatch 2.2207 + Moose::Exception::CloneObjectExpectsAnInstanceOfMetaclass 2.2207 + Moose::Exception::CodeBlockMustBeACodeRef 2.2207 + Moose::Exception::CoercingWithoutCoercions 2.2207 + Moose::Exception::CoercionAlreadyExists 2.2207 + Moose::Exception::CoercionNeedsTypeConstraint 2.2207 + Moose::Exception::ConflictDetectedInCheckRoleExclusions 2.2207 + Moose::Exception::ConflictDetectedInCheckRoleExclusionsInToClass 2.2207 + Moose::Exception::ConstructClassInstanceTakesPackageName 2.2207 + Moose::Exception::CouldNotCreateMethod 2.2207 + Moose::Exception::CouldNotCreateWriter 2.2207 + Moose::Exception::CouldNotEvalConstructor 2.2207 + Moose::Exception::CouldNotEvalDestructor 2.2207 + Moose::Exception::CouldNotFindTypeConstraintToCoerceFrom 2.2207 + Moose::Exception::CouldNotGenerateInlineAttributeMethod 2.2207 + Moose::Exception::CouldNotLocateTypeConstraintForUnion 2.2207 + Moose::Exception::CouldNotParseType 2.2207 + Moose::Exception::CreateMOPClassTakesArrayRefOfAttributes 2.2207 + Moose::Exception::CreateMOPClassTakesArrayRefOfSuperclasses 2.2207 + Moose::Exception::CreateMOPClassTakesHashRefOfMethods 2.2207 + Moose::Exception::CreateTakesArrayRefOfRoles 2.2207 + Moose::Exception::CreateTakesHashRefOfAttributes 2.2207 + Moose::Exception::CreateTakesHashRefOfMethods 2.2207 + Moose::Exception::DefaultToMatchOnTypeMustBeCodeRef 2.2207 + Moose::Exception::DelegationToAClassWhichIsNotLoaded 2.2207 + Moose::Exception::DelegationToARoleWhichIsNotLoaded 2.2207 + Moose::Exception::DelegationToATypeWhichIsNotAClass 2.2207 + Moose::Exception::DoesRequiresRoleName 2.2207 + Moose::Exception::EnumCalledWithAnArrayRefAndAdditionalArgs 2.2207 + Moose::Exception::EnumValuesMustBeString 2.2207 + Moose::Exception::ExtendsMissingArgs 2.2207 + Moose::Exception::HandlesMustBeAHashRef 2.2207 + Moose::Exception::IllegalInheritedOptions 2.2207 + Moose::Exception::IllegalMethodTypeToAddMethodModifier 2.2207 + Moose::Exception::IncompatibleMetaclassOfSuperclass 2.2207 + Moose::Exception::InitMetaRequiresClass 2.2207 + Moose::Exception::InitializeTakesUnBlessedPackageName 2.2207 + Moose::Exception::InstanceBlessedIntoWrongClass 2.2207 + Moose::Exception::InstanceMustBeABlessedReference 2.2207 + Moose::Exception::InvalidArgPassedToMooseUtilMetaRole 2.2207 + Moose::Exception::InvalidArgumentToMethod 2.2207 + Moose::Exception::InvalidArgumentsToTraitAliases 2.2207 + Moose::Exception::InvalidBaseTypeGivenToCreateParameterizedTypeConstraint 2.2207 + Moose::Exception::InvalidHandleValue 2.2207 + Moose::Exception::InvalidHasProvidedInARole 2.2207 + Moose::Exception::InvalidNameForType 2.2207 + Moose::Exception::InvalidOverloadOperator 2.2207 + Moose::Exception::InvalidRoleApplication 2.2207 + Moose::Exception::InvalidTypeConstraint 2.2207 + Moose::Exception::InvalidTypeGivenToCreateParameterizedTypeConstraint 2.2207 + Moose::Exception::InvalidValueForIs 2.2207 + Moose::Exception::IsaDoesNotDoTheRole 2.2207 + Moose::Exception::IsaLacksDoesMethod 2.2207 + Moose::Exception::LazyAttributeNeedsADefault 2.2207 + Moose::Exception::Legacy 2.2207 + Moose::Exception::MOPAttributeNewNeedsAttributeName 2.2207 + Moose::Exception::MatchActionMustBeACodeRef 2.2207 + Moose::Exception::MessageParameterMustBeCodeRef 2.2207 + Moose::Exception::MetaclassIsAClassNotASubclassOfGivenMetaclass 2.2207 + Moose::Exception::MetaclassIsARoleNotASubclassOfGivenMetaclass 2.2207 + Moose::Exception::MetaclassIsNotASubclassOfGivenMetaclass 2.2207 + Moose::Exception::MetaclassMustBeASubclassOfMooseMetaClass 2.2207 + Moose::Exception::MetaclassMustBeASubclassOfMooseMetaRole 2.2207 + Moose::Exception::MetaclassMustBeDerivedFromClassMOPClass 2.2207 + Moose::Exception::MetaclassNotLoaded 2.2207 + Moose::Exception::MetaclassTypeIncompatible 2.2207 + Moose::Exception::MethodExpectedAMetaclassObject 2.2207 + Moose::Exception::MethodExpectsFewerArgs 2.2207 + Moose::Exception::MethodExpectsMoreArgs 2.2207 + Moose::Exception::MethodModifierNeedsMethodName 2.2207 + Moose::Exception::MethodNameConflictInRoles 2.2207 + Moose::Exception::MethodNameNotFoundInInheritanceHierarchy 2.2207 + Moose::Exception::MethodNameNotGiven 2.2207 + Moose::Exception::MustDefineAMethodName 2.2207 + Moose::Exception::MustDefineAnAttributeName 2.2207 + Moose::Exception::MustDefineAnOverloadOperator 2.2207 + Moose::Exception::MustHaveAtLeastOneValueToEnumerate 2.2207 + Moose::Exception::MustPassAHashOfOptions 2.2207 + Moose::Exception::MustPassAMooseMetaRoleInstanceOrSubclass 2.2207 + Moose::Exception::MustPassAPackageNameOrAnExistingClassMOPPackageInstance 2.2207 + Moose::Exception::MustPassEvenNumberOfArguments 2.2207 + Moose::Exception::MustPassEvenNumberOfAttributeOptions 2.2207 + Moose::Exception::MustProvideANameForTheAttribute 2.2207 + Moose::Exception::MustSpecifyAtleastOneMethod 2.2207 + Moose::Exception::MustSpecifyAtleastOneRole 2.2207 + Moose::Exception::MustSpecifyAtleastOneRoleToApplicant 2.2207 + Moose::Exception::MustSupplyAClassMOPAttributeInstance 2.2207 + Moose::Exception::MustSupplyADelegateToMethod 2.2207 + Moose::Exception::MustSupplyAMetaclass 2.2207 + Moose::Exception::MustSupplyAMooseMetaAttributeInstance 2.2207 + Moose::Exception::MustSupplyAnAccessorTypeToConstructWith 2.2207 + Moose::Exception::MustSupplyAnAttributeToConstructWith 2.2207 + Moose::Exception::MustSupplyArrayRefAsCurriedArguments 2.2207 + Moose::Exception::MustSupplyPackageNameAndName 2.2207 + Moose::Exception::NeedsTypeConstraintUnionForTypeCoercionUnion 2.2207 + Moose::Exception::NeitherAttributeNorAttributeNameIsGiven 2.2207 + Moose::Exception::NeitherClassNorClassNameIsGiven 2.2207 + Moose::Exception::NeitherRoleNorRoleNameIsGiven 2.2207 + Moose::Exception::NeitherTypeNorTypeNameIsGiven 2.2207 + Moose::Exception::NoAttributeFoundInSuperClass 2.2207 + Moose::Exception::NoBodyToInitializeInAnAbstractBaseClass 2.2207 + Moose::Exception::NoCasesMatched 2.2207 + Moose::Exception::NoConstraintCheckForTypeConstraint 2.2207 + Moose::Exception::NoDestructorClassSpecified 2.2207 + Moose::Exception::NoImmutableTraitSpecifiedForClass 2.2207 + Moose::Exception::NoParentGivenToSubtype 2.2207 + Moose::Exception::OnlyInstancesCanBeCloned 2.2207 + Moose::Exception::OperatorIsRequired 2.2207 + Moose::Exception::OverloadConflictInSummation 2.2207 + Moose::Exception::OverloadRequiresAMetaClass 2.2207 + Moose::Exception::OverloadRequiresAMetaMethod 2.2207 + Moose::Exception::OverloadRequiresAMetaOverload 2.2207 + Moose::Exception::OverloadRequiresAMethodNameOrCoderef 2.2207 + Moose::Exception::OverloadRequiresAnOperator 2.2207 + Moose::Exception::OverloadRequiresNamesForCoderef 2.2207 + Moose::Exception::OverrideConflictInComposition 2.2207 + Moose::Exception::OverrideConflictInSummation 2.2207 + Moose::Exception::PackageDoesNotUseMooseExporter 2.2207 + Moose::Exception::PackageNameAndNameParamsNotGivenToWrap 2.2207 + Moose::Exception::PackagesAndModulesAreNotCachable 2.2207 + Moose::Exception::ParameterIsNotSubtypeOfParent 2.2207 + Moose::Exception::ReferencesAreNotAllowedAsDefault 2.2207 + Moose::Exception::RequiredAttributeLacksInitialization 2.2207 + Moose::Exception::RequiredAttributeNeedsADefault 2.2207 + Moose::Exception::RequiredMethodsImportedByClass 2.2207 + Moose::Exception::RequiredMethodsNotImplementedByClass 2.2207 + Moose::Exception::Role::Attribute 2.2207 + Moose::Exception::Role::AttributeName 2.2207 + Moose::Exception::Role::Class 2.2207 + Moose::Exception::Role::EitherAttributeOrAttributeName 2.2207 + Moose::Exception::Role::Instance 2.2207 + Moose::Exception::Role::InstanceClass 2.2207 + Moose::Exception::Role::InvalidAttributeOptions 2.2207 + Moose::Exception::Role::Method 2.2207 + Moose::Exception::Role::ParamsHash 2.2207 + Moose::Exception::Role::Role 2.2207 + Moose::Exception::Role::RoleForCreate 2.2207 + Moose::Exception::Role::RoleForCreateMOPClass 2.2207 + Moose::Exception::Role::TypeConstraint 2.2207 + Moose::Exception::RoleDoesTheExcludedRole 2.2207 + Moose::Exception::RoleExclusionConflict 2.2207 + Moose::Exception::RoleNameRequired 2.2207 + Moose::Exception::RoleNameRequiredForMooseMetaRole 2.2207 + Moose::Exception::RolesDoNotSupportAugment 2.2207 + Moose::Exception::RolesDoNotSupportExtends 2.2207 + Moose::Exception::RolesDoNotSupportInner 2.2207 + Moose::Exception::RolesDoNotSupportRegexReferencesForMethodModifiers 2.2207 + Moose::Exception::RolesInCreateTakesAnArrayRef 2.2207 + Moose::Exception::RolesListMustBeInstancesOfMooseMetaRole 2.2207 + Moose::Exception::SingleParamsToNewMustBeHashRef 2.2207 + Moose::Exception::TriggerMustBeACodeRef 2.2207 + Moose::Exception::TypeConstraintCannotBeUsedForAParameterizableType 2.2207 + Moose::Exception::TypeConstraintIsAlreadyCreated 2.2207 + Moose::Exception::TypeParameterMustBeMooseMetaType 2.2207 + Moose::Exception::UnableToCanonicalizeHandles 2.2207 + Moose::Exception::UnableToCanonicalizeNonRolePackage 2.2207 + Moose::Exception::UnableToRecognizeDelegateMetaclass 2.2207 + Moose::Exception::UndefinedHashKeysPassedToMethod 2.2207 + Moose::Exception::UnionCalledWithAnArrayRefAndAdditionalArgs 2.2207 + Moose::Exception::UnionTakesAtleastTwoTypeNames 2.2207 + Moose::Exception::ValidationFailedForInlineTypeConstraint 2.2207 + Moose::Exception::ValidationFailedForTypeConstraint 2.2207 + Moose::Exception::WrapTakesACodeRefToBless 2.2207 + Moose::Exception::WrongTypeConstraintGiven 2.2207 + Moose::Exporter 2.2207 + Moose::Intro 2.2207 + Moose::Manual 2.2207 + Moose::Manual::Attributes 2.2207 + Moose::Manual::BestPractices 2.2207 + Moose::Manual::Classes 2.2207 + Moose::Manual::Concepts 2.2207 + Moose::Manual::Construction 2.2207 + Moose::Manual::Contributing 2.2207 + Moose::Manual::Delegation 2.2207 + Moose::Manual::Delta 2.2207 + Moose::Manual::Exceptions 2.2207 + Moose::Manual::Exceptions::Manifest 2.2207 + Moose::Manual::FAQ 2.2207 + Moose::Manual::MOP 2.2207 + Moose::Manual::MethodModifiers 2.2207 + Moose::Manual::MooseX 2.2207 + Moose::Manual::Resources 2.2207 + Moose::Manual::Roles 2.2207 + Moose::Manual::Support 2.2207 + Moose::Manual::Types 2.2207 + Moose::Manual::Unsweetened 2.2207 + Moose::Meta::Attribute 2.2207 + Moose::Meta::Attribute::Native 2.2207 + Moose::Meta::Attribute::Native::Trait 2.2207 + Moose::Meta::Attribute::Native::Trait::Array 2.2207 + Moose::Meta::Attribute::Native::Trait::Bool 2.2207 + Moose::Meta::Attribute::Native::Trait::Code 2.2207 + Moose::Meta::Attribute::Native::Trait::Counter 2.2207 + Moose::Meta::Attribute::Native::Trait::Hash 2.2207 + Moose::Meta::Attribute::Native::Trait::Number 2.2207 + Moose::Meta::Attribute::Native::Trait::String 2.2207 + Moose::Meta::Class 2.2207 + Moose::Meta::Class::Immutable::Trait 2.2207 + Moose::Meta::Instance 2.2207 + Moose::Meta::Method 2.2207 + Moose::Meta::Method::Accessor 2.2207 + Moose::Meta::Method::Accessor::Native 2.2207 + Moose::Meta::Method::Accessor::Native::Array 2.2207 + Moose::Meta::Method::Accessor::Native::Array::Writer 2.2207 + Moose::Meta::Method::Accessor::Native::Array::accessor 2.2207 + Moose::Meta::Method::Accessor::Native::Array::clear 2.2207 + Moose::Meta::Method::Accessor::Native::Array::count 2.2207 + Moose::Meta::Method::Accessor::Native::Array::delete 2.2207 + Moose::Meta::Method::Accessor::Native::Array::elements 2.2207 + Moose::Meta::Method::Accessor::Native::Array::first 2.2207 + Moose::Meta::Method::Accessor::Native::Array::first_index 2.2207 + Moose::Meta::Method::Accessor::Native::Array::get 2.2207 + Moose::Meta::Method::Accessor::Native::Array::grep 2.2207 + Moose::Meta::Method::Accessor::Native::Array::insert 2.2207 + Moose::Meta::Method::Accessor::Native::Array::is_empty 2.2207 + Moose::Meta::Method::Accessor::Native::Array::join 2.2207 + Moose::Meta::Method::Accessor::Native::Array::map 2.2207 + Moose::Meta::Method::Accessor::Native::Array::natatime 2.2207 + Moose::Meta::Method::Accessor::Native::Array::pop 2.2207 + Moose::Meta::Method::Accessor::Native::Array::push 2.2207 + Moose::Meta::Method::Accessor::Native::Array::reduce 2.2207 + Moose::Meta::Method::Accessor::Native::Array::set 2.2207 + Moose::Meta::Method::Accessor::Native::Array::shallow_clone 2.2207 + Moose::Meta::Method::Accessor::Native::Array::shift 2.2207 + Moose::Meta::Method::Accessor::Native::Array::shuffle 2.2207 + Moose::Meta::Method::Accessor::Native::Array::sort 2.2207 + Moose::Meta::Method::Accessor::Native::Array::sort_in_place 2.2207 + Moose::Meta::Method::Accessor::Native::Array::splice 2.2207 + Moose::Meta::Method::Accessor::Native::Array::uniq 2.2207 + Moose::Meta::Method::Accessor::Native::Array::unshift 2.2207 + Moose::Meta::Method::Accessor::Native::Bool::not 2.2207 + Moose::Meta::Method::Accessor::Native::Bool::set 2.2207 + Moose::Meta::Method::Accessor::Native::Bool::toggle 2.2207 + Moose::Meta::Method::Accessor::Native::Bool::unset 2.2207 + Moose::Meta::Method::Accessor::Native::Code::execute 2.2207 + Moose::Meta::Method::Accessor::Native::Code::execute_method 2.2207 + Moose::Meta::Method::Accessor::Native::Collection 2.2207 + Moose::Meta::Method::Accessor::Native::Counter::Writer 2.2207 + Moose::Meta::Method::Accessor::Native::Counter::dec 2.2207 + Moose::Meta::Method::Accessor::Native::Counter::inc 2.2207 + Moose::Meta::Method::Accessor::Native::Counter::reset 2.2207 + Moose::Meta::Method::Accessor::Native::Counter::set 2.2207 + Moose::Meta::Method::Accessor::Native::Hash 2.2207 + Moose::Meta::Method::Accessor::Native::Hash::Writer 2.2207 + Moose::Meta::Method::Accessor::Native::Hash::accessor 2.2207 + Moose::Meta::Method::Accessor::Native::Hash::clear 2.2207 + Moose::Meta::Method::Accessor::Native::Hash::count 2.2207 + Moose::Meta::Method::Accessor::Native::Hash::defined 2.2207 + Moose::Meta::Method::Accessor::Native::Hash::delete 2.2207 + Moose::Meta::Method::Accessor::Native::Hash::elements 2.2207 + Moose::Meta::Method::Accessor::Native::Hash::exists 2.2207 + Moose::Meta::Method::Accessor::Native::Hash::get 2.2207 + Moose::Meta::Method::Accessor::Native::Hash::is_empty 2.2207 + Moose::Meta::Method::Accessor::Native::Hash::keys 2.2207 + Moose::Meta::Method::Accessor::Native::Hash::kv 2.2207 + Moose::Meta::Method::Accessor::Native::Hash::set 2.2207 + Moose::Meta::Method::Accessor::Native::Hash::shallow_clone 2.2207 + Moose::Meta::Method::Accessor::Native::Hash::values 2.2207 + Moose::Meta::Method::Accessor::Native::Number::abs 2.2207 + Moose::Meta::Method::Accessor::Native::Number::add 2.2207 + Moose::Meta::Method::Accessor::Native::Number::div 2.2207 + Moose::Meta::Method::Accessor::Native::Number::mod 2.2207 + Moose::Meta::Method::Accessor::Native::Number::mul 2.2207 + Moose::Meta::Method::Accessor::Native::Number::set 2.2207 + Moose::Meta::Method::Accessor::Native::Number::sub 2.2207 + Moose::Meta::Method::Accessor::Native::Reader 2.2207 + Moose::Meta::Method::Accessor::Native::String::append 2.2207 + Moose::Meta::Method::Accessor::Native::String::chomp 2.2207 + Moose::Meta::Method::Accessor::Native::String::chop 2.2207 + Moose::Meta::Method::Accessor::Native::String::clear 2.2207 + Moose::Meta::Method::Accessor::Native::String::inc 2.2207 + Moose::Meta::Method::Accessor::Native::String::length 2.2207 + Moose::Meta::Method::Accessor::Native::String::match 2.2207 + Moose::Meta::Method::Accessor::Native::String::prepend 2.2207 + Moose::Meta::Method::Accessor::Native::String::replace 2.2207 + Moose::Meta::Method::Accessor::Native::String::substr 2.2207 + Moose::Meta::Method::Accessor::Native::Writer 2.2207 + Moose::Meta::Method::Augmented 2.2207 + Moose::Meta::Method::Constructor 2.2207 + Moose::Meta::Method::Delegation 2.2207 + Moose::Meta::Method::Destructor 2.2207 + Moose::Meta::Method::Meta 2.2207 + Moose::Meta::Method::Overridden 2.2207 + Moose::Meta::Mixin::AttributeCore 2.2207 + Moose::Meta::Object::Trait 2.2207 + Moose::Meta::Role 2.2207 + Moose::Meta::Role::Application 2.2207 + Moose::Meta::Role::Application::RoleSummation 2.2207 + Moose::Meta::Role::Application::ToClass 2.2207 + Moose::Meta::Role::Application::ToInstance 2.2207 + Moose::Meta::Role::Application::ToRole 2.2207 + Moose::Meta::Role::Attribute 2.2207 + Moose::Meta::Role::Composite 2.2207 + Moose::Meta::Role::Method 2.2207 + Moose::Meta::Role::Method::Conflicting 2.2207 + Moose::Meta::Role::Method::Required 2.2207 + Moose::Meta::TypeCoercion 2.2207 + Moose::Meta::TypeCoercion::Union 2.2207 + Moose::Meta::TypeConstraint 2.2207 + Moose::Meta::TypeConstraint::Class 2.2207 + Moose::Meta::TypeConstraint::DuckType 2.2207 + Moose::Meta::TypeConstraint::Enum 2.2207 + Moose::Meta::TypeConstraint::Parameterizable 2.2207 + Moose::Meta::TypeConstraint::Parameterized 2.2207 + Moose::Meta::TypeConstraint::Registry 2.2207 + Moose::Meta::TypeConstraint::Role 2.2207 + Moose::Meta::TypeConstraint::Union 2.2207 + Moose::Object 2.2207 + Moose::Role 2.2207 + Moose::Spec::Role 2.2207 + Moose::Unsweetened 2.2207 + Moose::Util 2.2207 + Moose::Util::MetaRole 2.2207 + Moose::Util::TypeConstraints 2.2207 + Moose::Util::TypeConstraints::Builtins 2.2207 + Test::Moose 2.2207 + metaclass 2.2207 + oose 2.2207 + requirements: + Carp 1.22 + Class::Load 0.09 + Class::Load::XS 0.01 + Data::OptList 0.107 + Devel::GlobalDestruction 0 + Devel::OverloadInfo 0.005 + Devel::StackTrace 2.03 + Dist::CheckConflicts 0.02 + Eval::Closure 0.04 + ExtUtils::MakeMaker 0 + List::Util 1.56 + MRO::Compat 0.05 + Module::Runtime 0.014 + Module::Runtime::Conflicts 0.002 + Package::DeprecationManager 0.11 + Package::Stash 0.32 + Package::Stash::XS 0.24 + Params::Util 1.00 + Scalar::Util 1.19 + Sub::Exporter 0.980 + Sub::Util 1.40 + Try::Tiny 0.17 + parent 0.223 + strict 1.03 + warnings 1.03 + MooseX-Attribute-Chained-1.0.3 + pathname: T/TO/TOMHUKINS/MooseX-Attribute-Chained-1.0.3.tar.gz + provides: + Moose::Meta::Attribute::Custom::Trait::Chained v1.0.3 + MooseX::Attribute::Chained v1.0.3 + MooseX::Attribute::Chained::Method::Accessor v1.0.3 + MooseX::Attribute::ChainedClone v1.0.3 + MooseX::Attribute::ChainedClone::Method::Accessor v1.0.3 + MooseX::ChainedAccessors v1.0.3 + MooseX::ChainedAccessors::Accessor v1.0.3 + MooseX::Traits::Attribute::Chained v1.0.3 + MooseX::Traits::Attribute::ChainedClone v1.0.3 + requirements: + Module::Build 0.28 + Moose 0 + Test::More 0.88 + Try::Tiny 0 + MooseX-Attribute-Deflator-2.2.2 + pathname: P/PE/PERLER/MooseX-Attribute-Deflator-2.2.2.tar.gz + provides: + MooseX::Attribute::Deflator v2.2.2 + MooseX::Attribute::Deflator::Meta::Role::Attribute v2.2.2 + MooseX::Attribute::Deflator::Moose v2.2.2 + MooseX::Attribute::Deflator::Registry v2.2.2 + MooseX::Attribute::Deflator::Structured v2.2.2 + MooseX::Attribute::LazyInflator v2.2.2 + MooseX::Attribute::LazyInflator::Meta::Role::ApplicationToClass v2.2.2 + MooseX::Attribute::LazyInflator::Meta::Role::ApplicationToRole v2.2.2 + MooseX::Attribute::LazyInflator::Meta::Role::Attribute v2.2.2 + MooseX::Attribute::LazyInflator::Meta::Role::Composite v2.2.2 + MooseX::Attribute::LazyInflator::Meta::Role::Method::Accessor v2.2.2 + MooseX::Attribute::LazyInflator::Meta::Role::Method::Constructor v2.2.2 + MooseX::Attribute::LazyInflator::Meta::Role::Role v2.2.2 + MooseX::Attribute::LazyInflator::Role::Class v2.2.2 + requirements: + DateTime 0 + Devel::PartialDump 0 + File::Find 0 + File::Temp 0 + JSON 0 + Module::Build 0.3601 + Moose 1.25 + Moose::Util::TypeConstraints 0 + MooseX::Types 0.30 + MooseX::Types::Moose 0 + MooseX::Types::Structured 0 + Test::More 0.88 + Try::Tiny 0 + MooseX-Emulate-Class-Accessor-Fast-0.009032 + pathname: H/HA/HAARG/MooseX-Emulate-Class-Accessor-Fast-0.009032.tar.gz + provides: + MooseX::Adopt::Class::Accessor::Fast 0.009032 + MooseX::Emulate::Class::Accessor::Fast 0.009032 + MooseX::Emulate::Class::Accessor::Fast::Meta::Accessor undef + MooseX::Emulate::Class::Accessor::Fast::Meta::Role::Attribute undef + requirements: + ExtUtils::MakeMaker 0 + Moose 0.84 + namespace::clean 0 + MooseX-Fastly-Role-0.04 + pathname: L/LL/LLAP/MooseX-Fastly-Role-0.04.tar.gz + provides: + MooseX::Fastly::Role 0.04 + requirements: + Carp 0 + ExtUtils::MakeMaker 0 + HTTP::Tiny 0 + Moose::Role 0 + Net::Fastly 1.08 + MooseX-Getopt-0.78 + pathname: E/ET/ETHER/MooseX-Getopt-0.78.tar.gz + provides: + MooseX::Getopt 0.78 + MooseX::Getopt::Basic 0.78 + MooseX::Getopt::Dashes 0.78 + MooseX::Getopt::GLD 0.78 + MooseX::Getopt::Meta::Attribute 0.78 + MooseX::Getopt::Meta::Attribute::NoGetopt 0.78 + MooseX::Getopt::Meta::Attribute::Trait 0.78 + MooseX::Getopt::Meta::Attribute::Trait::NoGetopt 0.78 + MooseX::Getopt::OptionTypeMap 0.78 + MooseX::Getopt::ProcessedArgv 0.78 + MooseX::Getopt::Strict 0.78 + requirements: + Carp 0 + Getopt::Long 2.37 + Getopt::Long::Descriptive 0.088 + Module::Build::Tiny 0.034 + Moose 0 + Moose::Meta::Attribute 0 + Moose::Role 0.56 + Moose::Util::TypeConstraints 0 + MooseX::Role::Parameterized 1.01 + Scalar::Util 0 + Try::Tiny 0 + namespace::autoclean 0 + perl 5.006 + strict 0 + warnings 0 + MooseX-MethodAttributes-0.32 + pathname: E/ET/ETHER/MooseX-MethodAttributes-0.32.tar.gz + provides: + MooseX::MethodAttributes 0.32 + MooseX::MethodAttributes::Inheritable 0.32 + MooseX::MethodAttributes::Role 0.32 + MooseX::MethodAttributes::Role::AttrContainer 0.32 + MooseX::MethodAttributes::Role::AttrContainer::Inheritable 0.32 + MooseX::MethodAttributes::Role::Meta::Class 0.32 + MooseX::MethodAttributes::Role::Meta::Map 0.32 + MooseX::MethodAttributes::Role::Meta::Method 0.32 + MooseX::MethodAttributes::Role::Meta::Method::MaybeWrapped 0.32 + MooseX::MethodAttributes::Role::Meta::Method::Wrapped 0.32 + MooseX::MethodAttributes::Role::Meta::Role 0.32 + MooseX::MethodAttributes::Role::Meta::Role::Application 0.32 + MooseX::MethodAttributes::Role::Meta::Role::Application::Summation 0.32 + requirements: + Carp 0 + ExtUtils::MakeMaker 0 + Moose 0 + Moose::Exporter 0 + Moose::Role 0 + Moose::Util 0 + Moose::Util::MetaRole 0 + namespace::autoclean 0.08 + perl 5.006 + MooseX-Role-Parameterized-1.11 + pathname: E/ET/ETHER/MooseX-Role-Parameterized-1.11.tar.gz + provides: + MooseX::Role::Parameterised 1.11 + MooseX::Role::Parameterized 1.11 + MooseX::Role::Parameterized::Meta::Role::Parameterized 1.11 + MooseX::Role::Parameterized::Meta::Trait::Parameterizable 1.11 + MooseX::Role::Parameterized::Meta::Trait::Parameterized 1.11 + MooseX::Role::Parameterized::Parameters 1.11 + requirements: + Carp 0 + ExtUtils::MakeMaker 0 + Module::Build::Tiny 0.034 + Module::Runtime 0 + Moose 2.0300 + Moose::Exporter 0 + Moose::Meta::Role 0 + Moose::Role 0 + Moose::Util 0 + namespace::autoclean 0 + namespace::clean 0.19 + perl 5.008001 + strict 0 + warnings 0 + MooseX-StrictConstructor-0.21 + pathname: D/DR/DROLSKY/MooseX-StrictConstructor-0.21.tar.gz + provides: + MooseX::StrictConstructor 0.21 + MooseX::StrictConstructor::Trait::Class 0.21 + MooseX::StrictConstructor::Trait::Method::Constructor 0.21 + requirements: + B 0 + ExtUtils::MakeMaker 0 + Moose 0.94 + Moose::Exporter 0 + Moose::Role 0 + Moose::Util::MetaRole 0 + namespace::autoclean 0 + strict 0 + warnings 0 + MooseX-Types-0.51 + pathname: E/ET/ETHER/MooseX-Types-0.51.tar.gz + provides: + MooseX::Types 0.51 + MooseX::Types::Base 0.51 + MooseX::Types::CheckedUtilExports 0.51 + MooseX::Types::Combine 0.51 + MooseX::Types::Moose 0.51 + MooseX::Types::TypeDecorator 0.51 + MooseX::Types::UndefinedType 0.51 + MooseX::Types::Util 0.51 + MooseX::Types::Wrapper 0.51 + requirements: + Carp 0 + Carp::Clan 6.00 + Exporter 0 + Module::Build::Tiny 0.034 + Module::Runtime 0 + Moose 1.06 + Moose::Exporter 0 + Moose::Meta::TypeConstraint::Union 0 + Moose::Util::TypeConstraints 0 + Scalar::Util 1.19 + Sub::Exporter 0 + Sub::Exporter::ForMethods 0.100052 + Sub::Install 0 + Sub::Util 0 + base 0 + namespace::autoclean 0.16 + overload 0 + parent 0 + perl 5.008 + strict 0 + warnings 0 + MooseX-Types-ElasticSearch-0.0.4 + pathname: P/PE/PERLER/MooseX-Types-ElasticSearch-0.0.4.tar.gz + provides: + MooseX::Types::ElasticSearch v0.0.4 + requirements: + DateTime::Format::Epoch::Unix 0 + DateTime::Format::ISO8601 0 + Module::Build 0.3601 + MooseX::Types 0 + Search::Elasticsearch 0 + MooseX-Types-Structured-0.36 + pathname: E/ET/ETHER/MooseX-Types-Structured-0.36.tar.gz + provides: + MooseX::Types::Structured 0.36 + requirements: + Devel::PartialDump 0.13 + Module::Build::Tiny 0.034 + Moose 0 + Moose::Meta::TypeCoercion 0 + Moose::Meta::TypeConstraint 0 + Moose::Meta::TypeConstraint::Parameterizable 0 + Moose::Util::TypeConstraints 1.06 + MooseX::Types 0.22 + Scalar::Util 0 + Sub::Exporter 0.982 + if 0 + namespace::clean 0.19 + overload 0 + perl 5.008 + Mozilla-CA-20250202 + pathname: L/LW/LWP/Mozilla-CA-20250202.tar.gz + provides: + Mozilla::CA 20250202 + requirements: + ExtUtils::MakeMaker 0 + Net-DNS-1.50 + pathname: N/NL/NLNETLABS/Net-DNS-1.50.tar.gz + provides: + Net::DNS 1.50 + Net::DNS::Domain 2002 + Net::DNS::DomainName 2005 + Net::DNS::DomainName1035 2005 + Net::DNS::DomainName2535 2005 + Net::DNS::Header 2002 + Net::DNS::Mailbox 2002 + Net::DNS::Mailbox1035 2002 + Net::DNS::Mailbox2535 2002 + Net::DNS::Nameserver 2002 + Net::DNS::Packet 2003 + Net::DNS::Parameters 2002 + Net::DNS::Question 2002 + Net::DNS::RR 2003 + Net::DNS::RR::A 2003 + Net::DNS::RR::AAAA 2003 + Net::DNS::RR::AFSDB 2002 + Net::DNS::RR::AMTRELAY 2003 + Net::DNS::RR::APL 2003 + Net::DNS::RR::APL::Item 2003 + Net::DNS::RR::CAA 2003 + Net::DNS::RR::CDNSKEY 2003 + Net::DNS::RR::CDS 2003 + Net::DNS::RR::CERT 2002 + Net::DNS::RR::CNAME 2003 + Net::DNS::RR::CSYNC 2003 + Net::DNS::RR::DELEG 2003 + Net::DNS::RR::DHCID 2003 + Net::DNS::RR::DNAME 2003 + Net::DNS::RR::DNSKEY 2003 + Net::DNS::RR::DS 2003 + Net::DNS::RR::DSYNC 2003 + Net::DNS::RR::EUI48 2003 + Net::DNS::RR::EUI64 2003 + Net::DNS::RR::GPOS 2003 + Net::DNS::RR::HINFO 2003 + Net::DNS::RR::HIP 2003 + Net::DNS::RR::HTTPS 2002 + Net::DNS::RR::IPSECKEY 2003 + Net::DNS::RR::ISDN 2002 + Net::DNS::RR::KEY 2002 + Net::DNS::RR::KX 2003 + Net::DNS::RR::L32 2003 + Net::DNS::RR::L64 2003 + Net::DNS::RR::LOC 2003 + Net::DNS::RR::LP 2003 + Net::DNS::RR::MB 2002 + Net::DNS::RR::MG 2002 + Net::DNS::RR::MINFO 2002 + Net::DNS::RR::MR 2002 + Net::DNS::RR::MX 2002 + Net::DNS::RR::NAPTR 2003 + Net::DNS::RR::NID 2003 + Net::DNS::RR::NS 2003 + Net::DNS::RR::NSEC 2002 + Net::DNS::RR::NSEC3 2003 + Net::DNS::RR::NSEC3PARAM 2003 + Net::DNS::RR::NULL 2002 + Net::DNS::RR::OPENPGPKEY 2003 + Net::DNS::RR::OPT 2005 + Net::DNS::RR::OPT::CHAIN 2005 + Net::DNS::RR::OPT::CLIENT_SUBNET 2005 + Net::DNS::RR::OPT::COOKIE 2005 + Net::DNS::RR::OPT::DAU 2005 + Net::DNS::RR::OPT::DHU 2005 + Net::DNS::RR::OPT::EXPIRE 2005 + Net::DNS::RR::OPT::EXTENDED_ERROR 2005 + Net::DNS::RR::OPT::KEY_TAG 2005 + Net::DNS::RR::OPT::N3U 2005 + Net::DNS::RR::OPT::NSID 2005 + Net::DNS::RR::OPT::PADDING 2005 + Net::DNS::RR::OPT::REPORT_CHANNEL 2005 + Net::DNS::RR::OPT::TCP_KEEPALIVE 2005 + Net::DNS::RR::OPT::ZONEVERSION 2005 + Net::DNS::RR::PTR 2002 + Net::DNS::RR::PX 2003 + Net::DNS::RR::RESINFO 2003 + Net::DNS::RR::RP 2002 + Net::DNS::RR::RRSIG 2003 + Net::DNS::RR::RT 2003 + Net::DNS::RR::SIG 2003 + Net::DNS::RR::SMIMEA 2003 + Net::DNS::RR::SOA 2002 + Net::DNS::RR::SPF 2003 + Net::DNS::RR::SRV 2003 + Net::DNS::RR::SSHFP 2003 + Net::DNS::RR::SVCB 2003 + Net::DNS::RR::TKEY 2003 + Net::DNS::RR::TLSA 2003 + Net::DNS::RR::TSIG 2003 + Net::DNS::RR::TXT 2003 + Net::DNS::RR::URI 2003 + Net::DNS::RR::X25 2002 + Net::DNS::RR::ZONEMD 2003 + Net::DNS::Resolver 2009 + Net::DNS::Resolver::Base 2011 + Net::DNS::Resolver::MSWin32 2002 + Net::DNS::Resolver::Recurse 2002 + Net::DNS::Resolver::UNIX 2007 + Net::DNS::Resolver::android 2007 + Net::DNS::Resolver::cygwin 2002 + Net::DNS::Resolver::os2 2007 + Net::DNS::Resolver::os390 2007 + Net::DNS::Text 2002 + Net::DNS::Update 2003 + Net::DNS::ZoneFile 2002 + Net::DNS::ZoneFile::Generator 2002 + Net::DNS::ZoneFile::Text 2002 + requirements: + Carp 1.1 + Config 0 + Digest::HMAC 1.03 + Digest::MD5 2.37 + Digest::SHA 5.23 + Encode 2.26 + Exporter 5.63 + ExtUtils::MakeMaker 6.48 + File::Spec 3.29 + Getopt::Long 2.43 + IO::File 1.14 + IO::Select 1.17 + IO::Socket 1.3 + IO::Socket::IP 0.38 + MIME::Base64 3.07 + PerlIO 1.05 + Scalar::Util 1.19 + Socket 1.81 + Time::Local 1.19 + base 2.13 + constant 1.17 + integer 1 + overload 1.06 + perl 5.008009 + strict 1.03 + warnings 1.0501 + Net-Fastly-1.12 + pathname: F/FA/FASTLY/Net-Fastly-1.12.tar.gz + provides: + Net::Fastly 1.12 + Net::Fastly::Backend undef + Net::Fastly::BelongsToServiceAndVersion undef + Net::Fastly::Client undef + Net::Fastly::Client::UserAgent undef + Net::Fastly::Condition undef + Net::Fastly::Customer undef + Net::Fastly::Director undef + Net::Fastly::Domain undef + Net::Fastly::Healthcheck undef + Net::Fastly::Invoice undef + Net::Fastly::Match undef + Net::Fastly::Model undef + Net::Fastly::Origin undef + Net::Fastly::Service undef + Net::Fastly::Settings undef + Net::Fastly::Stats undef + Net::Fastly::Syslog undef + Net::Fastly::UA undef + Net::Fastly::User undef + Net::Fastly::VCL undef + Net::Fastly::Version undef + requirements: + Class::Accessor::Fast 0 + File::Basename 0 + File::Spec 0 + File::Temp 0 + IO::Socket::SSL != 1.38 + JSON::XS 0 + LWP::Protocol::https 0 + LWP::UserAgent 5.813 + Module::Build::Tiny 0.034 + Test::More 0 + URI 0 + URI::Escape 0 + YAML 0 + Net-GitHub-1.05 + pathname: F/FA/FAYLAND/Net-GitHub-1.05.tar.gz + provides: + Net::GitHub 1.05 + Net::GitHub::V3 1.05 + Net::GitHub::V3::Actions 1.05 + Net::GitHub::V3::Events 1.05 + Net::GitHub::V3::Gists 1.05 + Net::GitHub::V3::GitData 1.05 + Net::GitHub::V3::Gitignore 1.05 + Net::GitHub::V3::Issues 1.05 + Net::GitHub::V3::OAuth 1.05 + Net::GitHub::V3::Orgs 0.60 + Net::GitHub::V3::PullRequests 1.05 + Net::GitHub::V3::Query 1.05 + Net::GitHub::V3::Repos 1.05 + Net::GitHub::V3::ResultSet 1.05 + Net::GitHub::V3::Search 0.68 + Net::GitHub::V3::Users 1.05 + Net::GitHub::V4 1.05 + requirements: + Cache::LRU 0 + ExtUtils::MakeMaker 0 + HTTP::Request 0 + JSON::MaybeXS 0 + LWP::Protocol::https 0 + LWP::UserAgent 0 + MIME::Base64 0 + Moo 0 + Types::Standard 0 + URI 0 + URI::Escape 0 + Net-HTTP-6.23 + pathname: O/OA/OALDERS/Net-HTTP-6.23.tar.gz + provides: + Net::HTTP 6.23 + Net::HTTP::Methods 6.23 + Net::HTTP::NB 6.23 + Net::HTTPS 6.23 + requirements: + Carp 0 + Compress::Raw::Zlib 0 + ExtUtils::MakeMaker 0 + IO::Socket::INET 0 + IO::Uncompress::Gunzip 0 + URI 0 + base 0 + perl 5.006002 + strict 0 + warnings 0 + Net-IP-1.26 + pathname: M/MA/MANU/Net-IP-1.26.tar.gz + provides: + Net::IP 1.26 + requirements: + ExtUtils::MakeMaker 0 + Net-OAuth-0.31 + pathname: R/RR/RRWO/Net-OAuth-0.31.tar.gz + provides: + Net::OAuth 0.31 + Net::OAuth::AccessToken undef + Net::OAuth::AccessTokenRequest undef + Net::OAuth::AccessTokenResponse undef + Net::OAuth::Client 0.31 + Net::OAuth::ConsumerRequest undef + Net::OAuth::Message undef + Net::OAuth::ProtectedResourceRequest undef + Net::OAuth::Request 0.31 + Net::OAuth::RequestTokenRequest undef + Net::OAuth::RequestTokenResponse undef + Net::OAuth::Response undef + Net::OAuth::SignatureMethod::HMAC_SHA1 undef + Net::OAuth::SignatureMethod::HMAC_SHA256 undef + Net::OAuth::SignatureMethod::PLAINTEXT undef + Net::OAuth::SignatureMethod::RSA_SHA1 undef + Net::OAuth::UserAuthRequest undef + Net::OAuth::UserAuthResponse undef + Net::OAuth::V1_0A::AccessTokenRequest undef + Net::OAuth::V1_0A::RequestTokenRequest undef + Net::OAuth::V1_0A::RequestTokenResponse undef + Net::OAuth::V1_0A::UserAuthResponse undef + Net::OAuth::XauthAccessTokenRequest undef + Net::OAuth::YahooAccessTokenRefreshRequest undef + requirements: + Class::Accessor 0.31 + Class::Data::Inheritable 0.06 + Crypt::URandom 0.37 + Digest::SHA 5.47 + Encode 2.35 + ExtUtils::MakeMaker 0 + LWP::UserAgent 1 + Test::More 0.66 + Test::Warn 0.21 + URI 5.15 + Net-SSLeay-1.94 + pathname: C/CH/CHRISN/Net-SSLeay-1.94.tar.gz + provides: + Net::SSLeay 1.94 + Net::SSLeay::Handle 1.94 + requirements: + English 0 + ExtUtils::MakeMaker 0 + File::Spec::Functions 0 + MIME::Base64 0 + Text::Wrap 0 + constant 0 + perl 5.008001 + Number-Compare-0.03 + pathname: R/RC/RCLAMP/Number-Compare-0.03.tar.gz + provides: + Number::Compare 0.03 + requirements: + ExtUtils::MakeMaker 0 + Test::More 0 + Object-Signature-1.08 + pathname: E/ET/ETHER/Object-Signature-1.08.tar.gz + provides: + Object::Signature 1.08 + Object::Signature::File 1.08 + requirements: + Digest::MD5 2.00 + ExtUtils::MakeMaker 0 + Storable 2.11 + base 0 + perl 5.006 + strict 0 + warnings 0 + OrePAN2-0.52 + pathname: O/OA/OALDERS/OrePAN2-0.52.tar.gz + provides: + OrePAN2 0.52 + OrePAN2::Auditor undef + OrePAN2::CLI::Indexer undef + OrePAN2::CLI::Inject undef + OrePAN2::Index undef + OrePAN2::Indexer undef + OrePAN2::Injector undef + OrePAN2::Repository undef + OrePAN2::Repository::Cache undef + requirements: + Archive::Extract 0.72 + Archive::Tar 1.46 + CPAN::Meta 2.131560 + Digest::MD5 0 + ExtUtils::MakeMaker 7.06 + File::Path 0 + File::Spec 0 + File::Temp 0 + File::pushd 0 + File::stat 0 + Getopt::Long 2.39 + HTTP::Tiny 0 + IO::File::AtomicChange 0 + IO::Socket::SSL 1.42 + IO::Uncompress::Gunzip 0 + IO::Zlib 0 + JSON::PP 0 + LWP::UserAgent 0 + List::Compare 0 + MetaCPAN::Client 2.000000 + Module::Build::Tiny 0.035 + Moo 1.007000 + MooX::Options 0 + MooX::StrictConstructor 0 + Parse::CPAN::Meta 1.4414 + Parse::CPAN::Packages::Fast 0.09 + Parse::LocalDistribution 0.14 + Path::Tiny 0 + Pod::Usage 0 + Try::Tiny 0 + Type::Tiny 2.000000 + Types::Path::Tiny 0 + Types::Self 0 + Types::URI 0 + autodie 0 + feature 0 + namespace::clean 0 + parent 0 + perl 5.012000 + version 0.9912 + PAUSE-Permissions-0.17 + pathname: N/NE/NEILB/PAUSE-Permissions-0.17.tar.gz + provides: + PAUSE::Permissions 0.17 + PAUSE::Permissions::Entry 0.17 + PAUSE::Permissions::EntryIterator 0.17 + PAUSE::Permissions::Module 0.17 + PAUSE::Permissions::ModuleIterator 0.17 + requirements: + Carp 0 + ExtUtils::MakeMaker 0 + File::HomeDir 0 + File::Spec::Functions 0 + HTTP::Date 0 + HTTP::Tiny 0 + List::Util 1.33 + Moo 0 + MooX::Options 0 + Time::Duration::Parse 0 + autodie 0 + feature 0 + perl 5.010000 + strict 0 + warnings 0 + POSIX-strftime-Compiler-0.46 + pathname: K/KA/KAZEBURO/POSIX-strftime-Compiler-0.46.tar.gz + provides: + POSIX::strftime::Compiler 0.46 + requirements: + Carp 0 + Exporter 0 + Module::Build::Tiny 0.035 + POSIX 0 + Time::Local 0 + perl 5.008001 + PPI-1.281 + pathname: M/MI/MITHALDU/PPI-1.281.tar.gz + provides: + PPI 1.281 + PPI::Cache 1.281 + PPI::Document 1.281 + PPI::Document::File 1.281 + PPI::Document::Fragment 1.281 + PPI::Document::Normalized 1.281 + PPI::Dumper 1.281 + PPI::Element 1.281 + PPI::Exception 1.281 + PPI::Exception::ParserRejection 1.281 + PPI::Find 1.281 + PPI::Lexer 1.281 + PPI::Node 1.281 + PPI::Normal 1.281 + PPI::Normal::Standard 1.281 + PPI::Singletons 1.281 + PPI::Statement 1.281 + PPI::Statement::Break 1.281 + PPI::Statement::Compound 1.281 + PPI::Statement::Data 1.281 + PPI::Statement::End 1.281 + PPI::Statement::Expression 1.281 + PPI::Statement::Given 1.281 + PPI::Statement::Include 1.281 + PPI::Statement::Include::Perl6 1.281 + PPI::Statement::Null 1.281 + PPI::Statement::Package 1.281 + PPI::Statement::Scheduled 1.281 + PPI::Statement::Sub 1.281 + PPI::Statement::Unknown 1.281 + PPI::Statement::UnmatchedBrace 1.281 + PPI::Statement::Variable 1.281 + PPI::Statement::When 1.281 + PPI::Structure 1.281 + PPI::Structure::Block 1.281 + PPI::Structure::Condition 1.281 + PPI::Structure::Constructor 1.281 + PPI::Structure::For 1.281 + PPI::Structure::Given 1.281 + PPI::Structure::List 1.281 + PPI::Structure::Signature 1.281 + PPI::Structure::Subscript 1.281 + PPI::Structure::Unknown 1.281 + PPI::Structure::When 1.281 + PPI::Token 1.281 + PPI::Token::ArrayIndex 1.281 + PPI::Token::Attribute 1.281 + PPI::Token::BOM 1.281 + PPI::Token::Cast 1.281 + PPI::Token::Comment 1.281 + PPI::Token::DashedWord 1.281 + PPI::Token::Data 1.281 + PPI::Token::End 1.281 + PPI::Token::HereDoc 1.281 + PPI::Token::Label 1.281 + PPI::Token::Magic 1.281 + PPI::Token::Number 1.281 + PPI::Token::Number::Binary 1.281 + PPI::Token::Number::Exp 1.281 + PPI::Token::Number::Float 1.281 + PPI::Token::Number::Hex 1.281 + PPI::Token::Number::Octal 1.281 + PPI::Token::Number::Version 1.281 + PPI::Token::Operator 1.281 + PPI::Token::Pod 1.281 + PPI::Token::Prototype 1.281 + PPI::Token::Quote 1.281 + PPI::Token::Quote::Double 1.281 + PPI::Token::Quote::Interpolate 1.281 + PPI::Token::Quote::Literal 1.281 + PPI::Token::Quote::Single 1.281 + PPI::Token::QuoteLike 1.281 + PPI::Token::QuoteLike::Backtick 1.281 + PPI::Token::QuoteLike::Command 1.281 + PPI::Token::QuoteLike::Readline 1.281 + PPI::Token::QuoteLike::Regexp 1.281 + PPI::Token::QuoteLike::Words 1.281 + PPI::Token::Regexp 1.281 + PPI::Token::Regexp::Match 1.281 + PPI::Token::Regexp::Substitute 1.281 + PPI::Token::Regexp::Transliterate 1.281 + PPI::Token::Separator 1.281 + PPI::Token::Structure 1.281 + PPI::Token::Symbol 1.281 + PPI::Token::Unknown 1.281 + PPI::Token::Whitespace 1.281 + PPI::Token::Word 1.281 + PPI::Tokenizer 1.281 + PPI::Transform 1.281 + PPI::Transform::UpdateCopyright 1.281 + PPI::Util 1.281 + PPI::XSAccessor 1.281 + requirements: + Carp 0 + Clone 0.30 + Digest::MD5 2.35 + Exporter 0 + ExtUtils::MakeMaker 0 + File::Path 0 + File::Spec 0.84 + List::Util 1.33 + Params::Util 1.00 + Safe::Isa 0 + Scalar::Util 0 + Storable 2.17 + Task::Weaken 0 + YAML::PP 0 + constant 0 + if 0 + overload 0 + perl 5.006 + strict 0 + version 0.77 + PPIx-QuoteLike-0.023 + pathname: W/WY/WYANT/PPIx-QuoteLike-0.023.tar.gz + provides: + PPIx::QuoteLike 0.023 + PPIx::QuoteLike::Constant 0.023 + PPIx::QuoteLike::Dumper 0.023 + PPIx::QuoteLike::Token 0.023 + PPIx::QuoteLike::Token::Control 0.023 + PPIx::QuoteLike::Token::Delimiter 0.023 + PPIx::QuoteLike::Token::Interpolation 0.023 + PPIx::QuoteLike::Token::String 0.023 + PPIx::QuoteLike::Token::Structure 0.023 + PPIx::QuoteLike::Token::Unknown 0.023 + PPIx::QuoteLike::Token::Whitespace 0.023 + PPIx::QuoteLike::Utils 0.023 + requirements: + Carp 0 + Encode 0 + Exporter 0 + List::Util 0 + PPI::Document 1.238 + PPI::Dumper 1.238 + Readonly 0 + Scalar::Util 0 + Test::More 0.88 + base 0 + charnames 0 + constant 0 + lib 0 + perl 5.006 + re 0 + strict 0 + warnings 0 + PPIx-Regexp-0.088 + pathname: W/WY/WYANT/PPIx-Regexp-0.088.tar.gz + provides: + PPIx::Regexp 0.088 + PPIx::Regexp::Constant 0.085_04 + PPIx::Regexp::Constant::Inf 0.088 + PPIx::Regexp::Dumper 0.088 + PPIx::Regexp::Element 0.088 + PPIx::Regexp::Lexer 0.088 + PPIx::Regexp::Node 0.088 + PPIx::Regexp::Node::Range 0.088 + PPIx::Regexp::Node::Unknown 0.088 + PPIx::Regexp::Structure 0.088 + PPIx::Regexp::Structure::Assertion 0.088 + PPIx::Regexp::Structure::Atomic_Script_Run 0.088 + PPIx::Regexp::Structure::BranchReset 0.088 + PPIx::Regexp::Structure::Capture 0.088 + PPIx::Regexp::Structure::CharClass 0.088 + PPIx::Regexp::Structure::Code 0.088 + PPIx::Regexp::Structure::Main 0.088 + PPIx::Regexp::Structure::Modifier 0.088 + PPIx::Regexp::Structure::NamedCapture 0.088 + PPIx::Regexp::Structure::Quantifier 0.088 + PPIx::Regexp::Structure::RegexSet 0.088 + PPIx::Regexp::Structure::Regexp 0.088 + PPIx::Regexp::Structure::Replacement 0.088 + PPIx::Regexp::Structure::Script_Run 0.088 + PPIx::Regexp::Structure::Subexpression 0.088 + PPIx::Regexp::Structure::Switch 0.088 + PPIx::Regexp::Structure::Unknown 0.088 + PPIx::Regexp::Support 0.088 + PPIx::Regexp::Token 0.088 + PPIx::Regexp::Token::Assertion 0.088 + PPIx::Regexp::Token::Backreference 0.088 + PPIx::Regexp::Token::Backtrack 0.088 + PPIx::Regexp::Token::CharClass 0.088 + PPIx::Regexp::Token::CharClass::POSIX 0.088 + PPIx::Regexp::Token::CharClass::POSIX::Unknown 0.088 + PPIx::Regexp::Token::CharClass::Simple 0.088 + PPIx::Regexp::Token::Code 0.088 + PPIx::Regexp::Token::Comment 0.088 + PPIx::Regexp::Token::Condition 0.088 + PPIx::Regexp::Token::Control 0.088 + PPIx::Regexp::Token::Delimiter 0.088 + PPIx::Regexp::Token::Greediness 0.088 + PPIx::Regexp::Token::GroupType 0.088 + PPIx::Regexp::Token::GroupType::Assertion 0.088 + PPIx::Regexp::Token::GroupType::Atomic_Script_Run 0.088 + PPIx::Regexp::Token::GroupType::BranchReset 0.088 + PPIx::Regexp::Token::GroupType::Code 0.088 + PPIx::Regexp::Token::GroupType::Modifier 0.088 + PPIx::Regexp::Token::GroupType::NamedCapture 0.088 + PPIx::Regexp::Token::GroupType::Script_Run 0.088 + PPIx::Regexp::Token::GroupType::Subexpression 0.088 + PPIx::Regexp::Token::GroupType::Switch 0.088 + PPIx::Regexp::Token::Interpolation 0.088 + PPIx::Regexp::Token::Literal 0.088 + PPIx::Regexp::Token::Modifier 0.088 + PPIx::Regexp::Token::NoOp 0.088 + PPIx::Regexp::Token::Operator 0.088 + PPIx::Regexp::Token::Quantifier 0.088 + PPIx::Regexp::Token::Recursion 0.088 + PPIx::Regexp::Token::Reference 0.088 + PPIx::Regexp::Token::Structure 0.088 + PPIx::Regexp::Token::Unknown 0.088 + PPIx::Regexp::Token::Unmatched 0.088 + PPIx::Regexp::Token::Whitespace 0.088 + PPIx::Regexp::Tokenizer 0.088 + PPIx::Regexp::Util 0.088 + requirements: + Carp 0 + Encode 0 + Exporter 0 + List::Util 0 + PPI::Document 1.238 + PPI::Dumper 1.238 + Scalar::Util 0 + Task::Weaken 0 + Test::More 0.88 + base 0 + charnames 0 + constant 0 + lib 0 + overload 0 + perl 5.006 + strict 0 + warnings 0 + PPIx-Utils-0.003 + pathname: D/DB/DBOOK/PPIx-Utils-0.003.tar.gz + provides: + PPIx::Utils 0.003 + PPIx::Utils::Classification 0.003 + PPIx::Utils::Language 0.003 + PPIx::Utils::Traversal 0.003 + requirements: + B::Keywords 1.09 + Exporter 0 + ExtUtils::MakeMaker 0 + PPI 1.250 + Scalar::Util 0 + perl 5.006 + Package-DeprecationManager-0.18 + pathname: D/DR/DROLSKY/Package-DeprecationManager-0.18.tar.gz + provides: + Package::DeprecationManager 0.18 + requirements: + Carp 0 + ExtUtils::MakeMaker 0 + List::Util 1.33 + Package::Stash 0 + Params::Util 0 + Sub::Install 0 + Sub::Util 0 + strict 0 + warnings 0 + Package-Stash-0.40 + pathname: E/ET/ETHER/Package-Stash-0.40.tar.gz + provides: + Package::Stash 0.40 + Package::Stash::PP 0.40 + requirements: + B 0 + Carp 0 + Dist::CheckConflicts 0.02 + ExtUtils::MakeMaker 0 + Getopt::Long 0 + Module::Implementation 0.06 + Package::Stash::XS 0.26 + Scalar::Util 0 + Symbol 0 + Text::ParseWords 0 + constant 0 + perl 5.008001 + strict 0 + warnings 0 + Package-Stash-XS-0.30 + pathname: E/ET/ETHER/Package-Stash-XS-0.30.tar.gz + provides: + Package::Stash::XS 0.30 + requirements: + ExtUtils::MakeMaker 0 + XSLoader 0 + perl 5.008001 + strict 0 + warnings 0 + Parallel-ForkManager-2.03 + pathname: Y/YA/YANICK/Parallel-ForkManager-2.03.tar.gz + provides: + Parallel::ForkManager 2.03 + Parallel::ForkManager::Child 2.03 + requirements: + Carp 0 + ExtUtils::MakeMaker 0 + File::Path 0 + File::Spec 0 + File::Temp 0 + Moo 1.001000 + Moo::Role 0 + POSIX 0 + Storable 0 + perl 5.006 + strict 0 + warnings 0 + Params-Util-1.102 + pathname: R/RE/REHSACK/Params-Util-1.102.tar.gz + provides: + Params::Util 1.102 + Params::Util::PP 1.102 + requirements: + Carp 0 + ExtUtils::MakeMaker 0 + File::Basename 0 + File::Copy 0 + File::Path 0 + File::Spec 0 + IPC::Cmd 0 + Scalar::Util 1.18 + XSLoader 0.22 + parent 0 + Params-Validate-1.31 + pathname: D/DR/DROLSKY/Params-Validate-1.31.tar.gz + provides: + Params::Validate 1.31 + Params::Validate::Constants 1.31 + Params::Validate::PP 1.31 + Params::Validate::XS 1.31 + requirements: + Carp 0 + Exporter 0 + ExtUtils::CBuilder 0 + Module::Build 0.4227 + Module::Implementation 0 + Scalar::Util 1.10 + XSLoader 0 + perl 5.008001 + strict 0 + vars 0 + warnings 0 + Params-ValidationCompiler-0.31 + pathname: D/DR/DROLSKY/Params-ValidationCompiler-0.31.tar.gz + provides: + Params::ValidationCompiler 0.31 + Params::ValidationCompiler::Compiler 0.31 + Params::ValidationCompiler::Exceptions 0.31 + requirements: + B 0 + Carp 0 + Eval::Closure 0 + Exception::Class 0 + Exporter 0 + ExtUtils::MakeMaker 0 + List::Util 1.29 + Scalar::Util 0 + overload 0 + strict 0 + warnings 0 + Parse-CPAN-Packages-Fast-0.09 + pathname: S/SR/SREZIC/Parse-CPAN-Packages-Fast-0.09.tar.gz + provides: + Parse::CPAN::Packages::Fast 0.09 + Parse::CPAN::Packages::Fast::Distribution 0.09 + Parse::CPAN::Packages::Fast::Package 0.09 + requirements: + CPAN::DistnameInfo 0 + CPAN::Version 0 + ExtUtils::MakeMaker 0 + IO::Uncompress::Gunzip 0 + Parse-LocalDistribution-0.20 + pathname: I/IS/ISHIGAKI/Parse-LocalDistribution-0.20.tar.gz + provides: + Parse::LocalDistribution 0.20 + requirements: + ExtUtils::MakeMaker 0 + ExtUtils::MakeMaker::CPANfile 0.09 + File::Find 0 + File::Spec 0 + List::Util 0 + Parse::CPAN::Meta 0 + Parse::PMFile 0.37 + Parse-PMFile-0.47 + pathname: I/IS/ISHIGAKI/Parse-PMFile-0.47.tar.gz + provides: + Parse::PMFile 0.47 + requirements: + Dumpvalue 0 + ExtUtils::MakeMaker 0 + ExtUtils::MakeMaker::CPANfile 0.09 + File::Spec 0 + JSON::PP 2.00 + Safe 0 + version 0.83 + Path-Class-0.37 + pathname: K/KW/KWILLIAMS/Path-Class-0.37.tar.gz + provides: + Path::Class 0.37 + Path::Class::Dir 0.37 + Path::Class::Entity 0.37 + Path::Class::File 0.37 + requirements: + Carp 0 + Cwd 0 + Exporter 0 + ExtUtils::MakeMaker 6.30 + File::Copy 0 + File::Path 0 + File::Spec 3.26 + File::Temp 0 + File::stat 0 + IO::Dir 0 + IO::File 0 + Module::Build 0.3601 + Perl::OSType 0 + Scalar::Util 0 + overload 0 + parent 0 + strict 0 + Path-Iterator-Rule-1.015 + pathname: D/DA/DAGOLDEN/Path-Iterator-Rule-1.015.tar.gz + provides: + PIR 1.015 + Path::Iterator::Rule 1.015 + requirements: + Carp 0 + ExtUtils::MakeMaker 6.17 + File::Basename 0 + File::Spec 0 + List::Util 0 + Number::Compare 0.02 + Scalar::Util 0 + Text::Glob 0 + Try::Tiny 0 + if 0 + perl 5.008001 + strict 0 + warnings 0 + warnings::register 0 + Path-Tiny-0.148 + pathname: D/DA/DAGOLDEN/Path-Tiny-0.148.tar.gz + provides: + Path::Tiny 0.148 + Path::Tiny::Error 0.148 + requirements: + Carp 0 + Cwd 0 + Digest 1.03 + Digest::SHA 5.45 + Encode 0 + Exporter 5.57 + ExtUtils::MakeMaker 6.17 + Fcntl 0 + File::Compare 0 + File::Copy 0 + File::Glob 0 + File::Path 2.07 + File::Spec 0.86 + File::Temp 0.19 + File::stat 0 + constant 0 + overload 0 + perl 5.008001 + strict 0 + warnings 0 + warnings::register 0 + Perl-Critic-1.156 + pathname: P/PE/PETDANCE/Perl-Critic-1.156.tar.gz + provides: + Perl::Critic 1.156 + Perl::Critic::Annotation 1.156 + Perl::Critic::Command 1.156 + Perl::Critic::Config 1.156 + Perl::Critic::Document 1.156 + Perl::Critic::Exception 1.156 + Perl::Critic::Exception::AggregateConfiguration 1.156 + Perl::Critic::Exception::Configuration 1.156 + Perl::Critic::Exception::Configuration::Generic 1.156 + Perl::Critic::Exception::Configuration::NonExistentPolicy 1.156 + Perl::Critic::Exception::Configuration::Option 1.156 + Perl::Critic::Exception::Configuration::Option::Global 1.156 + Perl::Critic::Exception::Configuration::Option::Global::ExtraParameter 1.156 + Perl::Critic::Exception::Configuration::Option::Global::ParameterValue 1.156 + Perl::Critic::Exception::Configuration::Option::Policy 1.156 + Perl::Critic::Exception::Configuration::Option::Policy::ExtraParameter 1.156 + Perl::Critic::Exception::Configuration::Option::Policy::ParameterValue 1.156 + Perl::Critic::Exception::Fatal 1.156 + Perl::Critic::Exception::Fatal::Generic 1.156 + Perl::Critic::Exception::Fatal::Internal 1.156 + Perl::Critic::Exception::Fatal::PolicyDefinition 1.156 + Perl::Critic::Exception::IO 1.156 + Perl::Critic::Exception::Parse 1.156 + Perl::Critic::OptionsProcessor 1.156 + Perl::Critic::Policy 1.156 + Perl::Critic::Policy::BuiltinFunctions::ProhibitBooleanGrep 1.156 + Perl::Critic::Policy::BuiltinFunctions::ProhibitComplexMappings 1.156 + Perl::Critic::Policy::BuiltinFunctions::ProhibitLvalueSubstr 1.156 + Perl::Critic::Policy::BuiltinFunctions::ProhibitReverseSortBlock 1.156 + Perl::Critic::Policy::BuiltinFunctions::ProhibitShiftRef 1.156 + Perl::Critic::Policy::BuiltinFunctions::ProhibitSleepViaSelect 1.156 + Perl::Critic::Policy::BuiltinFunctions::ProhibitStringyEval 1.156 + Perl::Critic::Policy::BuiltinFunctions::ProhibitStringySplit 1.156 + Perl::Critic::Policy::BuiltinFunctions::ProhibitUniversalCan 1.156 + Perl::Critic::Policy::BuiltinFunctions::ProhibitUniversalIsa 1.156 + Perl::Critic::Policy::BuiltinFunctions::ProhibitUselessTopic 1.156 + Perl::Critic::Policy::BuiltinFunctions::ProhibitVoidGrep 1.156 + Perl::Critic::Policy::BuiltinFunctions::ProhibitVoidMap 1.156 + Perl::Critic::Policy::BuiltinFunctions::RequireBlockGrep 1.156 + Perl::Critic::Policy::BuiltinFunctions::RequireBlockMap 1.156 + Perl::Critic::Policy::BuiltinFunctions::RequireGlobFunction 1.156 + Perl::Critic::Policy::BuiltinFunctions::RequireSimpleSortBlock 1.156 + Perl::Critic::Policy::ClassHierarchies::ProhibitAutoloading 1.156 + Perl::Critic::Policy::ClassHierarchies::ProhibitExplicitISA 1.156 + Perl::Critic::Policy::ClassHierarchies::ProhibitOneArgBless 1.156 + Perl::Critic::Policy::CodeLayout::ProhibitHardTabs 1.156 + Perl::Critic::Policy::CodeLayout::ProhibitParensWithBuiltins 1.156 + Perl::Critic::Policy::CodeLayout::ProhibitQuotedWordLists 1.156 + Perl::Critic::Policy::CodeLayout::ProhibitTrailingWhitespace 1.156 + Perl::Critic::Policy::CodeLayout::RequireConsistentNewlines 1.156 + Perl::Critic::Policy::CodeLayout::RequireTidyCode 1.156 + Perl::Critic::Policy::CodeLayout::RequireTrailingCommas 1.156 + Perl::Critic::Policy::ControlStructures::ProhibitCStyleForLoops 1.156 + Perl::Critic::Policy::ControlStructures::ProhibitCascadingIfElse 1.156 + Perl::Critic::Policy::ControlStructures::ProhibitDeepNests 1.156 + Perl::Critic::Policy::ControlStructures::ProhibitLabelsWithSpecialBlockNames 1.156 + Perl::Critic::Policy::ControlStructures::ProhibitMutatingListFunctions 1.156 + Perl::Critic::Policy::ControlStructures::ProhibitNegativeExpressionsInUnlessAndUntilConditions 1.156 + Perl::Critic::Policy::ControlStructures::ProhibitPostfixControls 1.156 + Perl::Critic::Policy::ControlStructures::ProhibitUnlessBlocks 1.156 + Perl::Critic::Policy::ControlStructures::ProhibitUnreachableCode 1.156 + Perl::Critic::Policy::ControlStructures::ProhibitUntilBlocks 1.156 + Perl::Critic::Policy::ControlStructures::ProhibitYadaOperator 1.156 + Perl::Critic::Policy::Documentation::PodSpelling 1.156 + Perl::Critic::Policy::Documentation::RequirePackageMatchesPodName 1.156 + Perl::Critic::Policy::Documentation::RequirePodAtEnd 1.156 + Perl::Critic::Policy::Documentation::RequirePodSections 1.156 + Perl::Critic::Policy::ErrorHandling::RequireCarping 1.156 + Perl::Critic::Policy::ErrorHandling::RequireCheckingReturnValueOfEval 1.156 + Perl::Critic::Policy::InputOutput::ProhibitBacktickOperators 1.156 + Perl::Critic::Policy::InputOutput::ProhibitBarewordDirHandles 1.156 + Perl::Critic::Policy::InputOutput::ProhibitBarewordFileHandles 1.156 + Perl::Critic::Policy::InputOutput::ProhibitExplicitStdin 1.156 + Perl::Critic::Policy::InputOutput::ProhibitInteractiveTest 1.156 + Perl::Critic::Policy::InputOutput::ProhibitJoinedReadline 1.156 + Perl::Critic::Policy::InputOutput::ProhibitOneArgSelect 1.156 + Perl::Critic::Policy::InputOutput::ProhibitReadlineInForLoop 1.156 + Perl::Critic::Policy::InputOutput::ProhibitTwoArgOpen 1.156 + Perl::Critic::Policy::InputOutput::RequireBracedFileHandleWithPrint 1.156 + Perl::Critic::Policy::InputOutput::RequireBriefOpen 1.156 + Perl::Critic::Policy::InputOutput::RequireCheckedClose 1.156 + Perl::Critic::Policy::InputOutput::RequireCheckedOpen 1.156 + Perl::Critic::Policy::InputOutput::RequireCheckedSyscalls 1.156 + Perl::Critic::Policy::InputOutput::RequireEncodingWithUTF8Layer 1.156 + Perl::Critic::Policy::Miscellanea::ProhibitFormats 1.156 + Perl::Critic::Policy::Miscellanea::ProhibitTies 1.156 + Perl::Critic::Policy::Miscellanea::ProhibitUnrestrictedNoCritic 1.156 + Perl::Critic::Policy::Miscellanea::ProhibitUselessNoCritic 1.156 + Perl::Critic::Policy::Modules::ProhibitAutomaticExportation 1.156 + Perl::Critic::Policy::Modules::ProhibitConditionalUseStatements 1.156 + Perl::Critic::Policy::Modules::ProhibitEvilModules 1.156 + Perl::Critic::Policy::Modules::ProhibitExcessMainComplexity 1.156 + Perl::Critic::Policy::Modules::ProhibitMultiplePackages 1.156 + Perl::Critic::Policy::Modules::RequireBarewordIncludes 1.156 + Perl::Critic::Policy::Modules::RequireEndWithOne 1.156 + Perl::Critic::Policy::Modules::RequireExplicitPackage 1.156 + Perl::Critic::Policy::Modules::RequireFilenameMatchesPackage 1.156 + Perl::Critic::Policy::Modules::RequireNoMatchVarsWithUseEnglish 1.156 + Perl::Critic::Policy::Modules::RequireVersionVar 1.156 + Perl::Critic::Policy::NamingConventions::Capitalization 1.156 + Perl::Critic::Policy::NamingConventions::ProhibitAmbiguousNames 1.156 + Perl::Critic::Policy::Objects::ProhibitIndirectSyntax 1.156 + Perl::Critic::Policy::References::ProhibitDoubleSigils 1.156 + Perl::Critic::Policy::RegularExpressions::ProhibitCaptureWithoutTest 1.156 + Perl::Critic::Policy::RegularExpressions::ProhibitComplexRegexes 1.156 + Perl::Critic::Policy::RegularExpressions::ProhibitEnumeratedClasses 1.156 + Perl::Critic::Policy::RegularExpressions::ProhibitEscapedMetacharacters 1.156 + Perl::Critic::Policy::RegularExpressions::ProhibitFixedStringMatches 1.156 + Perl::Critic::Policy::RegularExpressions::ProhibitSingleCharAlternation 1.156 + Perl::Critic::Policy::RegularExpressions::ProhibitUnusedCapture 1.156 + Perl::Critic::Policy::RegularExpressions::ProhibitUnusualDelimiters 1.156 + Perl::Critic::Policy::RegularExpressions::ProhibitUselessTopic 1.156 + Perl::Critic::Policy::RegularExpressions::RequireBracesForMultiline 1.156 + Perl::Critic::Policy::RegularExpressions::RequireDotMatchAnything 1.156 + Perl::Critic::Policy::RegularExpressions::RequireExtendedFormatting 1.156 + Perl::Critic::Policy::RegularExpressions::RequireLineBoundaryMatching 1.156 + Perl::Critic::Policy::Subroutines::ProhibitAmpersandSigils 1.156 + Perl::Critic::Policy::Subroutines::ProhibitBuiltinHomonyms 1.156 + Perl::Critic::Policy::Subroutines::ProhibitExcessComplexity 1.156 + Perl::Critic::Policy::Subroutines::ProhibitExplicitReturnUndef 1.156 + Perl::Critic::Policy::Subroutines::ProhibitManyArgs 1.156 + Perl::Critic::Policy::Subroutines::ProhibitNestedSubs 1.156 + Perl::Critic::Policy::Subroutines::ProhibitReturnSort 1.156 + Perl::Critic::Policy::Subroutines::ProhibitSubroutinePrototypes 1.156 + Perl::Critic::Policy::Subroutines::ProhibitUnusedPrivateSubroutines 1.156 + Perl::Critic::Policy::Subroutines::ProtectPrivateSubs 1.156 + Perl::Critic::Policy::Subroutines::RequireArgUnpacking 1.156 + Perl::Critic::Policy::Subroutines::RequireFinalReturn 1.156 + Perl::Critic::Policy::TestingAndDebugging::ProhibitNoStrict 1.156 + Perl::Critic::Policy::TestingAndDebugging::ProhibitNoWarnings 1.156 + Perl::Critic::Policy::TestingAndDebugging::ProhibitProlongedStrictureOverride 1.156 + Perl::Critic::Policy::TestingAndDebugging::RequireTestLabels 1.156 + Perl::Critic::Policy::TestingAndDebugging::RequireUseStrict 1.156 + Perl::Critic::Policy::TestingAndDebugging::RequireUseWarnings 1.156 + Perl::Critic::Policy::ValuesAndExpressions::ProhibitCommaSeparatedStatements 1.156 + Perl::Critic::Policy::ValuesAndExpressions::ProhibitComplexVersion 1.156 + Perl::Critic::Policy::ValuesAndExpressions::ProhibitConstantPragma 1.156 + Perl::Critic::Policy::ValuesAndExpressions::ProhibitEmptyQuotes 1.156 + Perl::Critic::Policy::ValuesAndExpressions::ProhibitEscapedCharacters 1.156 + Perl::Critic::Policy::ValuesAndExpressions::ProhibitImplicitNewlines 1.156 + Perl::Critic::Policy::ValuesAndExpressions::ProhibitInterpolationOfLiterals 1.156 + Perl::Critic::Policy::ValuesAndExpressions::ProhibitLeadingZeros 1.156 + Perl::Critic::Policy::ValuesAndExpressions::ProhibitLongChainsOfMethodCalls 1.156 + Perl::Critic::Policy::ValuesAndExpressions::ProhibitMagicNumbers 1.156 + Perl::Critic::Policy::ValuesAndExpressions::ProhibitMismatchedOperators 1.156 + Perl::Critic::Policy::ValuesAndExpressions::ProhibitMixedBooleanOperators 1.156 + Perl::Critic::Policy::ValuesAndExpressions::ProhibitNoisyQuotes 1.156 + Perl::Critic::Policy::ValuesAndExpressions::ProhibitQuotesAsQuotelikeOperatorDelimiters 1.156 + Perl::Critic::Policy::ValuesAndExpressions::ProhibitSpecialLiteralHeredocTerminator 1.156 + Perl::Critic::Policy::ValuesAndExpressions::ProhibitVersionStrings 1.156 + Perl::Critic::Policy::ValuesAndExpressions::RequireConstantVersion 1.156 + Perl::Critic::Policy::ValuesAndExpressions::RequireInterpolationOfMetachars 1.156 + Perl::Critic::Policy::ValuesAndExpressions::RequireNumberSeparators 1.156 + Perl::Critic::Policy::ValuesAndExpressions::RequireQuotedHeredocTerminator 1.156 + Perl::Critic::Policy::ValuesAndExpressions::RequireUpperCaseHeredocTerminator 1.156 + Perl::Critic::Policy::Variables::ProhibitAugmentedAssignmentInDeclaration 1.156 + Perl::Critic::Policy::Variables::ProhibitConditionalDeclarations 1.156 + Perl::Critic::Policy::Variables::ProhibitEvilVariables 1.156 + Perl::Critic::Policy::Variables::ProhibitLocalVars 1.156 + Perl::Critic::Policy::Variables::ProhibitMatchVars 1.156 + Perl::Critic::Policy::Variables::ProhibitPackageVars 1.156 + Perl::Critic::Policy::Variables::ProhibitPerl4PackageNames 1.156 + Perl::Critic::Policy::Variables::ProhibitPunctuationVars 1.156 + Perl::Critic::Policy::Variables::ProhibitReusedNames 1.156 + Perl::Critic::Policy::Variables::ProhibitUnusedVariables 1.156 + Perl::Critic::Policy::Variables::ProtectPrivateVars 1.156 + Perl::Critic::Policy::Variables::RequireInitializationForLocalVars 1.156 + Perl::Critic::Policy::Variables::RequireLexicalLoopIterators 1.156 + Perl::Critic::Policy::Variables::RequireLocalizedPunctuationVars 1.156 + Perl::Critic::Policy::Variables::RequireNegativeIndices 1.156 + Perl::Critic::PolicyConfig 1.156 + Perl::Critic::PolicyFactory 1.156 + Perl::Critic::PolicyListing 1.156 + Perl::Critic::PolicyParameter 1.156 + Perl::Critic::PolicyParameter::Behavior 1.156 + Perl::Critic::PolicyParameter::Behavior::Boolean 1.156 + Perl::Critic::PolicyParameter::Behavior::Enumeration 1.156 + Perl::Critic::PolicyParameter::Behavior::Integer 1.156 + Perl::Critic::PolicyParameter::Behavior::String 1.156 + Perl::Critic::PolicyParameter::Behavior::StringList 1.156 + Perl::Critic::ProfilePrototype 1.156 + Perl::Critic::Statistics 1.156 + Perl::Critic::TestUtils 1.156 + Perl::Critic::Theme 1.156 + Perl::Critic::ThemeListing 1.156 + Perl::Critic::UserProfile 1.156 + Perl::Critic::Utils 1.156 + Perl::Critic::Utils::Constants 1.156 + Perl::Critic::Utils::McCabe 1.156 + Perl::Critic::Utils::POD 1.156 + Perl::Critic::Utils::PPI 1.156 + Perl::Critic::Utils::Perl 1.156 + Perl::Critic::Violation 1.156 + Test::Perl::Critic::Policy 1.156 + requirements: + B::Keywords 1.23 + Carp 0 + Config::Tiny 2 + English 0 + Exception::Class 1.23 + Exporter 5.63 + File::Basename 0 + File::Find 0 + File::Path 0 + File::Spec 0 + File::Spec::Unix 0 + File::Temp 0 + File::Which 0 + Getopt::Long 0 + List::SomeUtils 0.55 + List::Util 0 + Module::Build 0.4204 + Module::Pluggable 3.1 + PPI 1.277 + PPI::Document 1.277 + PPI::Document::File 1.277 + PPI::Node 1.277 + PPI::Token::Quote::Single 1.277 + PPI::Token::Whitespace 1.277 + PPIx::QuoteLike 0 + PPIx::Regexp 0.027 + PPIx::Regexp::Util 0.068 + PPIx::Utils::Traversal 0.003 + Perl::Tidy 0 + Pod::PlainText 0 + Pod::Select 0 + Pod::Spell 1 + Pod::Usage 0 + Readonly 2 + Scalar::Util 0 + String::Format 1.18 + Term::ANSIColor 2.02 + Test::Builder 0.92 + Text::ParseWords 3 + base 0 + charnames 0 + lib 0 + overload 0 + parent 0 + perl 5.010001 + strict 0 + version 0.77 + warnings 0 + Perl-Tidy-20240511 + pathname: S/SH/SHANCOCK/Perl-Tidy-20240511.tar.gz + provides: + Perl::Tidy 20240511 + Perl::Tidy::Debugger 20240511 + Perl::Tidy::Diagnostics 20240511 + Perl::Tidy::FileWriter 20240511 + Perl::Tidy::Formatter 20240511 + Perl::Tidy::HtmlWriter 20240511 + Perl::Tidy::IOScalar 20240511 + Perl::Tidy::IOScalarArray 20240511 + Perl::Tidy::IndentationItem 20240511 + Perl::Tidy::Logger 20240511 + Perl::Tidy::Tokenizer 20240511 + Perl::Tidy::VerticalAligner 20240511 + Perl::Tidy::VerticalAligner::Alignment 20240511 + Perl::Tidy::VerticalAligner::Line 20240511 + requirements: + ExtUtils::MakeMaker 0 + perl 5.008 + PerlIO-gzip-0.20 + pathname: N/NW/NWCLARK/PerlIO-gzip-0.20.tar.gz + provides: + PerlIO::gzip 0.20 + requirements: + ExtUtils::MakeMaker 0 + PerlIO-utf8_strict-0.010 + pathname: L/LE/LEONT/PerlIO-utf8_strict-0.010.tar.gz + provides: + PerlIO::utf8_strict 0.010 + requirements: + ExtUtils::MakeMaker 0 + XSLoader 0 + perl 5.008 + strict 0 + warnings 0 + Plack-1.0051 + pathname: M/MI/MIYAGAWA/Plack-1.0051.tar.gz + provides: + HTTP::Message::PSGI undef + HTTP::Server::PSGI undef + Plack 1.0051 + Plack::App::CGIBin undef + Plack::App::Cascade undef + Plack::App::Directory undef + Plack::App::File undef + Plack::App::PSGIBin undef + Plack::App::URLMap undef + Plack::App::WrapCGI undef + Plack::Builder undef + Plack::Component undef + Plack::HTTPParser undef + Plack::HTTPParser::PP undef + Plack::Handler undef + Plack::Handler::Apache1 undef + Plack::Handler::Apache2 undef + Plack::Handler::Apache2::Registry undef + Plack::Handler::CGI undef + Plack::Handler::CGI::Writer undef + Plack::Handler::FCGI undef + Plack::Handler::HTTP::Server::PSGI undef + Plack::Handler::Standalone undef + Plack::LWPish undef + Plack::Loader undef + Plack::Loader::Delayed undef + Plack::Loader::Restarter undef + Plack::Loader::Shotgun undef + Plack::MIME undef + Plack::Middleware undef + Plack::Middleware::AccessLog undef + Plack::Middleware::AccessLog::Timed undef + Plack::Middleware::Auth::Basic undef + Plack::Middleware::BufferedStreaming undef + Plack::Middleware::Chunked undef + Plack::Middleware::Conditional undef + Plack::Middleware::ConditionalGET undef + Plack::Middleware::ContentLength undef + Plack::Middleware::ContentMD5 undef + Plack::Middleware::ErrorDocument undef + Plack::Middleware::HTTPExceptions undef + Plack::Middleware::Head undef + Plack::Middleware::IIS6ScriptNameFix undef + Plack::Middleware::IIS7KeepAliveFix undef + Plack::Middleware::JSONP undef + Plack::Middleware::LighttpdScriptNameFix undef + Plack::Middleware::Lint undef + Plack::Middleware::Log4perl undef + Plack::Middleware::LogDispatch undef + Plack::Middleware::NullLogger undef + Plack::Middleware::RearrangeHeaders undef + Plack::Middleware::Recursive undef + Plack::Middleware::Refresh undef + Plack::Middleware::Runtime undef + Plack::Middleware::SimpleContentFilter undef + Plack::Middleware::SimpleLogger undef + Plack::Middleware::StackTrace undef + Plack::Middleware::Static undef + Plack::Middleware::XFramework undef + Plack::Middleware::XSendfile undef + Plack::Recursive::ForwardRequest undef + Plack::Request 1.0051 + Plack::Request::Upload undef + Plack::Response 1.0051 + Plack::Runner undef + Plack::TempBuffer undef + Plack::Test undef + Plack::Test::MockHTTP undef + Plack::Test::Server undef + Plack::Test::Suite undef + Plack::Util undef + Plack::Util::Accessor undef + Plack::Util::IOWithPath undef + Plack::Util::Prototype undef + requirements: + Apache::LogFormat::Compiler 0.33 + Cookie::Baker 0.07 + Devel::StackTrace 1.23 + Devel::StackTrace::AsHTML 0.11 + ExtUtils::MakeMaker 0 + File::ShareDir 1.00 + File::ShareDir::Install 0.06 + Filesys::Notify::Simple 0 + HTTP::Entity::Parser 0.25 + HTTP::Headers::Fast 0.18 + HTTP::Message 5.814 + HTTP::Tiny 0.034 + Hash::MultiValue 0.05 + Pod::Usage 1.36 + Stream::Buffered 0.02 + Test::TCP 2.15 + Try::Tiny 0 + URI 1.59 + WWW::Form::UrlEncoded 0.23 + parent 0 + perl 5.012000 + Plack-Middleware-FixMissingBodyInRedirect-0.12 + pathname: S/SW/SWEETKID/Plack-Middleware-FixMissingBodyInRedirect-0.12.tar.gz + provides: + Plack::Middleware::FixMissingBodyInRedirect 0.12 + requirements: + ExtUtils::MakeMaker 6.30 + HTML::Entities 0 + Plack::Middleware 0 + Plack::Util 0 + Scalar::Util 0 + parent 0 + strict 0 + warnings 0 + Plack-Middleware-MethodOverride-0.20 + pathname: M/MI/MIYAGAWA/Plack-Middleware-MethodOverride-0.20.tar.gz + provides: + Plack::Middleware::MethodOverride 0.20 + requirements: + ExtUtils::MakeMaker 0 + Plack::Middleware 0 + Plack::Request 0 + Plack::Util::Accessor 0 + parent 0 + perl 5.008001 + strict 0 + warnings 0 + Plack-Middleware-RemoveRedundantBody-0.09 + pathname: S/SW/SWEETKID/Plack-Middleware-RemoveRedundantBody-0.09.tar.gz + provides: + Plack::Middleware::RemoveRedundantBody 0.09 + requirements: + ExtUtils::MakeMaker 0 + Plack::Middleware 0 + Plack::Util 0 + parent 0 + strict 0 + warnings 0 + Plack-Middleware-ReverseProxy-0.16 + pathname: M/MI/MIYAGAWA/Plack-Middleware-ReverseProxy-0.16.tar.gz + provides: + Plack::Middleware::ReverseProxy 0.16 + requirements: + ExtUtils::MakeMaker 6.59 + Plack 0.9988 + Plack::Middleware 0 + Plack::Request 0 + Test::More 0 + parent 0 + perl 5.008001 + Plack-Middleware-Session-0.34 + pathname: M/MI/MIYAGAWA/Plack-Middleware-Session-0.34.tar.gz + provides: + Plack::Middleware::Session 0.34 + Plack::Middleware::Session::Cookie undef + Plack::Session 0.34 + Plack::Session::Cleanup 0.34 + Plack::Session::State 0.34 + Plack::Session::State::Cookie 0.34 + Plack::Session::Store 0.34 + Plack::Session::Store::Cache 0.34 + Plack::Session::Store::DBI 0.34 + Plack::Session::Store::File 0.34 + Plack::Session::Store::Null 0.34 + requirements: + Cookie::Baker 0.12 + Digest::HMAC_SHA1 1.03 + Digest::SHA 0 + Module::Build::Tiny 0.034 + Plack 0.9910 + Plack-Test-ExternalServer-0.02 + pathname: E/ET/ETHER/Plack-Test-ExternalServer-0.02.tar.gz + provides: + Plack::Test::ExternalServer 0.02 + requirements: + Carp 0 + ExtUtils::MakeMaker 0 + LWP::UserAgent 0 + URI 0 + perl 5.006 + strict 0 + warnings 0 + Pod-Markdown-3.400 + pathname: R/RW/RWSTAUNER/Pod-Markdown-3.400.tar.gz + provides: + Pod::Markdown 3.400 + Pod::Perldoc::ToMarkdown 3.400 + requirements: + Encode 0 + ExtUtils::MakeMaker 0 + Getopt::Long 0 + Pod::Simple 3.27 + Pod::Simple::Methody 0 + Pod::Usage 0 + URI::Escape 0 + parent 0 + perl 5.008 + strict 0 + warnings 0 + Pod-Simple-3.45 + pathname: K/KH/KHW/Pod-Simple-3.45.tar.gz + provides: + Pod::Simple 3.45 + Pod::Simple::BlackBox 3.45 + Pod::Simple::Checker 3.45 + Pod::Simple::Debug 3.45 + Pod::Simple::DumpAsText 3.45 + Pod::Simple::DumpAsXML 3.45 + Pod::Simple::HTML 3.45 + Pod::Simple::HTMLBatch 3.45 + Pod::Simple::HTMLLegacy 5.01 + Pod::Simple::JustPod undef + Pod::Simple::LinkSection 3.45 + Pod::Simple::Methody 3.45 + Pod::Simple::Progress 3.45 + Pod::Simple::PullParser 3.45 + Pod::Simple::PullParserEndToken 3.45 + Pod::Simple::PullParserStartToken 3.45 + Pod::Simple::PullParserTextToken 3.45 + Pod::Simple::PullParserToken 3.45 + Pod::Simple::RTF 3.45 + Pod::Simple::Search 3.45 + Pod::Simple::SimpleTree 3.45 + Pod::Simple::Text 3.45 + Pod::Simple::TextContent 3.45 + Pod::Simple::TiedOutFH 3.45 + Pod::Simple::Transcode 3.45 + Pod::Simple::TranscodeDumb 3.45 + Pod::Simple::TranscodeSmart 3.45 + Pod::Simple::XHTML 3.45 + Pod::Simple::XMLOutStream 3.45 + requirements: + Carp 0 + Config 0 + Cwd 0 + ExtUtils::MakeMaker 0 + File::Basename 0 + File::Find 0 + File::Spec 0 + Pod::Escapes 1.04 + Symbol 0 + Text::Wrap 98.112902 + if 0 + integer 0 + overload 0 + strict 0 + Pod-Spell-1.27 + pathname: H/HA/HAARG/Pod-Spell-1.27.tar.gz + provides: + Pod::Spell 1.27 + Pod::Wordlist 1.27 + requirements: + Carp 0 + Class::Tiny 0 + ExtUtils::MakeMaker 0 + File::ShareDir 0 + File::ShareDir::Install 0.06 + Lingua::EN::Inflect 0 + POSIX 0 + Pod::Escapes 0 + Pod::Simple 3.27 + Text::Wrap 0 + constant 0 + locale 0 + parent 0 + perl 5.008001 + Readonly-2.05 + pathname: S/SA/SANKO/Readonly-2.05.tar.gz + provides: + Readonly 2.05 + Readonly::Array undef + Readonly::Hash undef + Readonly::Scalar undef + requirements: + Module::Build::Tiny 0.035 + perl 5.005 + Ref-Util-0.204 + pathname: A/AR/ARC/Ref-Util-0.204.tar.gz + provides: + Ref::Util 0.204 + Ref::Util::PP 0.204 + requirements: + Exporter 5.57 + ExtUtils::MakeMaker 0 + Ref::Util::XS 0 + Text::ParseWords 0 + perl 5.006 + Ref-Util-XS-0.117 + pathname: X/XS/XSAWYERX/Ref-Util-XS-0.117.tar.gz + provides: + Ref::Util::XS 0.117 + requirements: + Exporter 5.57 + ExtUtils::MakeMaker 0 + XSLoader 0 + perl 5.006 + Role-Hooks-0.008 + pathname: T/TO/TOBYINK/Role-Hooks-0.008.tar.gz + provides: + Role::Hooks 0.008 + requirements: + Class::Method::Modifiers 0 + ExtUtils::MakeMaker 6.17 + List::Util 1.45 + perl 5.008001 + Role-Tiny-2.002004 + pathname: H/HA/HAARG/Role-Tiny-2.002004.tar.gz + provides: + Role::Tiny 2.002004 + Role::Tiny::With 2.002004 + requirements: + Exporter 5.57 + perl 5.006 + SQL-Abstract-2.000001 + pathname: M/MS/MSTROUT/SQL-Abstract-2.000001.tar.gz + provides: + Chunkstrumenter undef + DBIx::Class::SQLMaker::Role::SQLA2Passthrough undef + SQL::Abstract 2.000001 + SQL::Abstract::Formatter undef + SQL::Abstract::Parts undef + SQL::Abstract::Plugin::BangOverrides undef + SQL::Abstract::Plugin::ExtraClauses undef + SQL::Abstract::Reference undef + SQL::Abstract::Role::Plugin undef + SQL::Abstract::Test undef + SQL::Abstract::Tree undef + requirements: + Exporter 5.57 + ExtUtils::MakeMaker 0 + Hash::Merge 0.12 + List::Util 0 + MRO::Compat 0.12 + Moo 2.000001 + Scalar::Util 0 + Sub::Quote 2.000001 + Test::Builder::Module 0.84 + Test::Deep 0.101 + Text::Balanced 2.00 + perl 5.006 + SQL-Abstract-Pg-1.0 + pathname: S/SR/SRI/SQL-Abstract-Pg-1.0.tar.gz + provides: + SQL::Abstract::Pg 1.0 + requirements: + ExtUtils::MakeMaker 0 + SQL::Abstract 2.0 + perl 5.016 + Safe-Isa-1.000010 + pathname: E/ET/ETHER/Safe-Isa-1.000010.tar.gz + provides: + Safe::Isa 1.000010 + requirements: + Exporter 5.57 + ExtUtils::MakeMaker 0 + Scalar::Util 0 + perl 5.006 + Scalar-List-Utils-1.69 + pathname: P/PE/PEVANS/Scalar-List-Utils-1.69.tar.gz + provides: + List::Util 1.69 + List::Util::XS 1.69 + Scalar::List::Utils 1.69 + Scalar::Util 1.69 + Sub::Util 1.69 + requirements: + ExtUtils::MakeMaker 0 + perl 5.006 + Scope-Guard-0.21 + pathname: C/CH/CHOCOLATE/Scope-Guard-0.21.tar.gz + provides: + Scope::Guard 0.21 + requirements: + ExtUtils::MakeMaker 0 + Test::More 0 + perl 5.006001 + Search-Elasticsearch-8.12 + pathname: E/EZ/EZIMUEL/Search-Elasticsearch-8.12.tar.gz + provides: + Search::Elasticsearch 8.12 + Search::Elasticsearch::Client::8_0 8.12 + Search::Elasticsearch::Client::8_0::Bulk 8.12 + Search::Elasticsearch::Client::8_0::Direct 8.12 + Search::Elasticsearch::Client::8_0::Direct::Autoscaling 8.12 + Search::Elasticsearch::Client::8_0::Direct::CCR 8.12 + Search::Elasticsearch::Client::8_0::Direct::Cat 8.12 + Search::Elasticsearch::Client::8_0::Direct::Cluster 8.12 + Search::Elasticsearch::Client::8_0::Direct::Connector 8.12 + Search::Elasticsearch::Client::8_0::Direct::ConnectorSyncJob 8.12 + Search::Elasticsearch::Client::8_0::Direct::DanglingIndices 8.12 + Search::Elasticsearch::Client::8_0::Direct::Enrich 8.12 + Search::Elasticsearch::Client::8_0::Direct::Eql 8.12 + Search::Elasticsearch::Client::8_0::Direct::Esql 8.12 + Search::Elasticsearch::Client::8_0::Direct::Features 8.12 + Search::Elasticsearch::Client::8_0::Direct::Fleet 8.12 + Search::Elasticsearch::Client::8_0::Direct::Graph 8.12 + Search::Elasticsearch::Client::8_0::Direct::ILM 8.12 + Search::Elasticsearch::Client::8_0::Direct::Indices 8.12 + Search::Elasticsearch::Client::8_0::Direct::Inference 8.12 + Search::Elasticsearch::Client::8_0::Direct::Ingest 8.12 + Search::Elasticsearch::Client::8_0::Direct::License 8.12 + Search::Elasticsearch::Client::8_0::Direct::Logstash 8.12 + Search::Elasticsearch::Client::8_0::Direct::ML 8.12 + Search::Elasticsearch::Client::8_0::Direct::Migration 8.12 + Search::Elasticsearch::Client::8_0::Direct::Monitoring 8.12 + Search::Elasticsearch::Client::8_0::Direct::Nodes 8.12 + Search::Elasticsearch::Client::8_0::Direct::Profiling 8.12 + Search::Elasticsearch::Client::8_0::Direct::QueryRuleset 8.12 + Search::Elasticsearch::Client::8_0::Direct::Rollup 8.12 + Search::Elasticsearch::Client::8_0::Direct::SQL 8.12 + Search::Elasticsearch::Client::8_0::Direct::SSL 8.12 + Search::Elasticsearch::Client::8_0::Direct::SearchApplication 8.12 + Search::Elasticsearch::Client::8_0::Direct::SearchableSnapshots 8.12 + Search::Elasticsearch::Client::8_0::Direct::Security 8.12 + Search::Elasticsearch::Client::8_0::Direct::Shutdown 8.12 + Search::Elasticsearch::Client::8_0::Direct::Simulate 8.12 + Search::Elasticsearch::Client::8_0::Direct::Slm 8.12 + Search::Elasticsearch::Client::8_0::Direct::Snapshot 8.12 + Search::Elasticsearch::Client::8_0::Direct::Synonyms 8.12 + Search::Elasticsearch::Client::8_0::Direct::Tasks 8.12 + Search::Elasticsearch::Client::8_0::Direct::TextStructure 8.12 + Search::Elasticsearch::Client::8_0::Direct::Transform 8.12 + Search::Elasticsearch::Client::8_0::Direct::Watcher 8.12 + Search::Elasticsearch::Client::8_0::Direct::XPack 8.12 + Search::Elasticsearch::Client::8_0::Role::API 8.12 + Search::Elasticsearch::Client::8_0::Role::Bulk 8.12 + Search::Elasticsearch::Client::8_0::Role::Scroll 8.12 + Search::Elasticsearch::Client::8_0::Scroll 8.12 + Search::Elasticsearch::Client::8_0::TestServer 8.12 + Search::Elasticsearch::Cxn::Factory 8.12 + Search::Elasticsearch::Cxn::HTTPTiny 8.12 + Search::Elasticsearch::Cxn::LWP 8.12 + Search::Elasticsearch::CxnPool::Sniff 8.12 + Search::Elasticsearch::CxnPool::Static 8.12 + Search::Elasticsearch::CxnPool::Static::NoPing 8.12 + Search::Elasticsearch::Error 8.12 + Search::Elasticsearch::Logger::LogAny 8.12 + Search::Elasticsearch::Role::API 8.12 + Search::Elasticsearch::Role::Client 8.12 + Search::Elasticsearch::Role::Client::Direct 8.12 + Search::Elasticsearch::Role::Cxn 8.12 + Search::Elasticsearch::Role::CxnPool 8.12 + Search::Elasticsearch::Role::CxnPool::Sniff 8.12 + Search::Elasticsearch::Role::CxnPool::Static 8.12 + Search::Elasticsearch::Role::CxnPool::Static::NoPing 8.12 + Search::Elasticsearch::Role::Is_Sync 8.12 + Search::Elasticsearch::Role::Logger 8.12 + Search::Elasticsearch::Role::Serializer 8.12 + Search::Elasticsearch::Role::Serializer::JSON 8.12 + Search::Elasticsearch::Role::Transport 8.12 + Search::Elasticsearch::Serializer::JSON 8.12 + Search::Elasticsearch::Serializer::JSON::Cpanel 8.12 + Search::Elasticsearch::Serializer::JSON::PP 8.12 + Search::Elasticsearch::Serializer::JSON::XS 8.12 + Search::Elasticsearch::TestServer 8.12 + Search::Elasticsearch::Transport 8.12 + Search::Elasticsearch::Util 8.12 + requirements: + Any::URI::Escape 0 + Data::Dumper 0 + Devel::GlobalDestruction 0 + Encode 0 + ExtUtils::MakeMaker 0 + File::Temp 0 + HTTP::Headers 0 + HTTP::Request 0 + HTTP::Tiny 0.076 + IO::Compress::Deflate 0 + IO::Compress::Gzip 0 + IO::Select 0 + IO::Socket 0 + IO::Uncompress::Gunzip 0 + IO::Uncompress::Inflate 0 + JSON::MaybeXS 1.002002 + JSON::PP 0 + LWP::UserAgent 0 + List::Util 0 + Log::Any 1.02 + Log::Any::Adapter 0 + MIME::Base64 0 + Module::Runtime 0 + Moo 2.001000 + Moo::Role 0 + Net::IP 0 + POSIX 0 + Package::Stash 0.34 + Scalar::Util 0 + Sub::Exporter 0 + Time::HiRes 0 + Try::Tiny 0 + URI 0 + namespace::clean 0 + overload 0 + strict 0 + warnings 0 + Search-Elasticsearch-Client-2_0-6.81 + pathname: E/EZ/EZIMUEL/Search-Elasticsearch-Client-2_0-6.81.tar.gz + provides: + Search::Elasticsearch::Client::2_0 6.81 + Search::Elasticsearch::Client::2_0::Bulk 6.81 + Search::Elasticsearch::Client::2_0::Direct 6.81 + Search::Elasticsearch::Client::2_0::Direct::Cat 6.81 + Search::Elasticsearch::Client::2_0::Direct::Cluster 6.81 + Search::Elasticsearch::Client::2_0::Direct::Indices 6.81 + Search::Elasticsearch::Client::2_0::Direct::Nodes 6.81 + Search::Elasticsearch::Client::2_0::Direct::Snapshot 6.81 + Search::Elasticsearch::Client::2_0::Direct::Tasks 6.81 + Search::Elasticsearch::Client::2_0::Role::API 6.81 + Search::Elasticsearch::Client::2_0::Role::Bulk 6.81 + Search::Elasticsearch::Client::2_0::Role::Scroll 6.81 + Search::Elasticsearch::Client::2_0::Scroll 6.81 + Search::Elasticsearch::Client::2_0::TestServer 6.81 + requirements: + Devel::GlobalDestruction 0 + ExtUtils::MakeMaker 0 + Moo 0 + Moo::Role 0 + Search::Elasticsearch 6.00 + Search::Elasticsearch::Role::API 0 + Search::Elasticsearch::Role::Client::Direct 0 + Search::Elasticsearch::Role::Is_Sync 0 + Search::Elasticsearch::Util 0 + Try::Tiny 0 + namespace::clean 0 + strict 0 + warnings 0 + Sereal-Decoder-5.004 + pathname: Y/YV/YVES/Sereal-Decoder-5.004.tar.gz + provides: + Sereal::Decoder 5.004 + Sereal::Decoder::Constants 5.004 + Sereal::Performance undef + requirements: + Data::Dumper 0 + Devel::CheckLib 1.16 + ExtUtils::MakeMaker 7.0 + ExtUtils::ParseXS 2.21 + File::Find 0 + File::Path 0 + File::Spec 0 + Scalar::Util 0 + Test::Deep 0 + Test::Differences 0 + Test::LongString 0 + Test::More 0.88 + Test::Warn 0 + XSLoader 0 + perl 5.008 + Sereal-Encoder-5.004 + pathname: Y/YV/YVES/Sereal-Encoder-5.004.tar.gz + provides: + Sereal::Encoder 5.004 + Sereal::Encoder::Constants 5.004 + requirements: + Data::Dumper 0 + Devel::CheckLib 1.16 + ExtUtils::MakeMaker 7.0 + ExtUtils::ParseXS 2.21 + File::Find 0 + File::Path 0 + File::Spec 0 + Hash::Util 0 + Scalar::Util 0 + Sereal::Decoder 5.004 + Test::Deep 0 + Test::Differences 0 + Test::LongString 0 + Test::More 0.88 + Test::Warn 0 + XSLoader 0 + perl 5.008 + Sort-Versions-1.62 + pathname: N/NE/NEILB/Sort-Versions-1.62.tar.gz + provides: + Sort::Versions 1.62 + requirements: + Exporter 0 + ExtUtils::MakeMaker 0 + perl 5.006 + strict 0 + warnings 0 + Specio-0.50 + pathname: D/DR/DROLSKY/Specio-0.50.tar.gz + provides: + Specio 0.50 + Specio::Coercion 0.50 + Specio::Constraint::AnyCan 0.50 + Specio::Constraint::AnyDoes 0.50 + Specio::Constraint::AnyIsa 0.50 + Specio::Constraint::Enum 0.50 + Specio::Constraint::Intersection 0.50 + Specio::Constraint::ObjectCan 0.50 + Specio::Constraint::ObjectDoes 0.50 + Specio::Constraint::ObjectIsa 0.50 + Specio::Constraint::Parameterizable 0.50 + Specio::Constraint::Parameterized 0.50 + Specio::Constraint::Role::CanType 0.50 + Specio::Constraint::Role::DoesType 0.50 + Specio::Constraint::Role::Interface 0.50 + Specio::Constraint::Role::IsaType 0.50 + Specio::Constraint::Simple 0.50 + Specio::Constraint::Structurable 0.50 + Specio::Constraint::Structured 0.50 + Specio::Constraint::Union 0.50 + Specio::Declare 0.50 + Specio::DeclaredAt 0.50 + Specio::Exception 0.50 + Specio::Exporter 0.50 + Specio::Helpers 0.50 + Specio::Library::Builtins 0.50 + Specio::Library::Numeric 0.50 + Specio::Library::Perl 0.50 + Specio::Library::String 0.50 + Specio::Library::Structured 0.50 + Specio::Library::Structured::Dict 0.50 + Specio::Library::Structured::Map 0.50 + Specio::Library::Structured::Tuple 0.50 + Specio::OO 0.50 + Specio::PartialDump 0.50 + Specio::Registry 0.50 + Specio::Role::Inlinable 0.50 + Specio::Subs 0.50 + Specio::TypeChecks 0.50 + Test::Specio 0.50 + requirements: + B 0 + Carp 0 + Clone 0 + Devel::StackTrace 0 + Eval::Closure 0 + Exporter 0 + ExtUtils::MakeMaker 0 + IO::File 0 + List::Util 1.33 + MRO::Compat 0 + Module::Runtime 0 + Role::Tiny 1.003003 + Role::Tiny::With 0 + Scalar::Util 0 + Sub::Quote 0 + Test::Fatal 0 + Test::More 0.96 + Try::Tiny 0 + XString 0 + overload 0 + parent 0 + perl 5.008 + re 0 + strict 0 + version 0.83 + warnings 0 + Specio-Library-Path-Tiny-0.05 + pathname: D/DR/DROLSKY/Specio-Library-Path-Tiny-0.05.tar.gz + provides: + Specio::Library::Path::Tiny 0.05 + requirements: + ExtUtils::MakeMaker 0 + Path::Tiny 0.087 + Scalar::Util 0 + Specio 0.29 + Specio::Declare 0 + Specio::Exporter 0 + Specio::Library::Builtins 0 + Specio::PartialDump 0 + overload 0 + parent 0 + strict 0 + warnings 0 + Stream-Buffered-0.03 + pathname: D/DO/DOY/Stream-Buffered-0.03.tar.gz + provides: + Stream::Buffered 0.03 + Stream::Buffered::Auto undef + Stream::Buffered::File undef + Stream::Buffered::PerlIO undef + requirements: + ExtUtils::MakeMaker 6.30 + IO::File 1.14 + String-Format-1.18 + pathname: S/SR/SREZIC/String-Format-1.18.tar.gz + provides: + String::Format 1.18 + requirements: + ExtUtils::MakeMaker 0 + Test::More 0 + String-RewritePrefix-0.009 + pathname: R/RJ/RJBS/String-RewritePrefix-0.009.tar.gz + provides: + String::RewritePrefix 0.009 + requirements: + Carp 0 + ExtUtils::MakeMaker 6.78 + Sub::Exporter 0.972 + perl 5.012 + strict 0 + warnings 0 + Sub-Exporter-0.991 + pathname: R/RJ/RJBS/Sub-Exporter-0.991.tar.gz + provides: + Sub::Exporter 0.991 + Sub::Exporter::Util 0.991 + requirements: + Carp 0 + Data::OptList 0.100 + ExtUtils::MakeMaker 6.78 + Params::Util 0.14 + Sub::Install 0.92 + perl 5.012 + strict 0 + warnings 0 + Sub-Exporter-ForMethods-0.100055 + pathname: R/RJ/RJBS/Sub-Exporter-ForMethods-0.100055.tar.gz + provides: + Sub::Exporter::ForMethods 0.100055 + requirements: + ExtUtils::MakeMaker 6.78 + Scalar::Util 0 + Sub::Exporter 0.978 + Sub::Util 0 + perl 5.012 + strict 0 + warnings 0 + Sub-Exporter-Progressive-0.001013 + pathname: F/FR/FREW/Sub-Exporter-Progressive-0.001013.tar.gz + provides: + Sub::Exporter::Progressive 0.001013 + requirements: + ExtUtils::MakeMaker 0 + Sub-HandlesVia-0.050002 + pathname: T/TO/TOBYINK/Sub-HandlesVia-0.050002.tar.gz + provides: + Sub::HandlesVia 0.050002 + Sub::HandlesVia::CodeGenerator 0.050002 + Sub::HandlesVia::Declare 0.050002 + Sub::HandlesVia::Handler 0.050002 + Sub::HandlesVia::Handler::CodeRef 0.050002 + Sub::HandlesVia::Handler::Traditional 0.050002 + Sub::HandlesVia::HandlerLibrary 0.050002 + Sub::HandlesVia::HandlerLibrary::Array 0.050002 + Sub::HandlesVia::HandlerLibrary::Blessed 0.050002 + Sub::HandlesVia::HandlerLibrary::Bool 0.050002 + Sub::HandlesVia::HandlerLibrary::Code 0.050002 + Sub::HandlesVia::HandlerLibrary::Counter 0.050002 + Sub::HandlesVia::HandlerLibrary::Enum 0.050002 + Sub::HandlesVia::HandlerLibrary::Hash 0.050002 + Sub::HandlesVia::HandlerLibrary::Number 0.050002 + Sub::HandlesVia::HandlerLibrary::Scalar 0.050002 + Sub::HandlesVia::HandlerLibrary::String 0.050002 + Sub::HandlesVia::Mite undef + Sub::HandlesVia::Toolkit 0.050002 + Sub::HandlesVia::Toolkit::Mite 0.050002 + Sub::HandlesVia::Toolkit::Moo 0.050002 + Sub::HandlesVia::Toolkit::Moose 0.050002 + Sub::HandlesVia::Toolkit::Moose::PackageTrait 0.050002 + Sub::HandlesVia::Toolkit::Moose::RoleTrait 0.050002 + Sub::HandlesVia::Toolkit::Mouse 0.050002 + Sub::HandlesVia::Toolkit::Mouse::PackageTrait 0.050002 + Sub::HandlesVia::Toolkit::Mouse::RoleTrait 0.050002 + Sub::HandlesVia::Toolkit::ObjectPad 0.050002 + Sub::HandlesVia::Toolkit::Plain 0.050002 + requirements: + Class::Method::Modifiers 0 + Exporter::Shiny 0 + ExtUtils::MakeMaker 6.17 + List::Util 1.54 + Role::Hooks 0.008 + Role::Tiny 0 + Type::Tiny 1.004 + perl 5.008001 + Sub-Install-0.929 + pathname: R/RJ/RJBS/Sub-Install-0.929.tar.gz + provides: + Sub::Install 0.929 + requirements: + B 0 + Carp 0 + ExtUtils::MakeMaker 6.78 + Scalar::Util 0 + perl 5.008000 + strict 0 + warnings 0 + Sub-Quote-2.006008 + pathname: H/HA/HAARG/Sub-Quote-2.006008.tar.gz + provides: + Sub::Defer 2.006008 + Sub::Quote 2.006008 + requirements: + ExtUtils::MakeMaker 0 + Scalar::Util 0 + perl 5.006 + Sub-Uplevel-0.2800 + pathname: D/DA/DAGOLDEN/Sub-Uplevel-0.2800.tar.gz + provides: + Sub::Uplevel 0.2800 + requirements: + Carp 0 + ExtUtils::MakeMaker 6.17 + constant 0 + perl 5.006 + strict 0 + warnings 0 + Symbol-Get-0.12 + pathname: F/FE/FELIPE/Symbol-Get-0.12.tar.gz + provides: + Symbol::Get 0.12 + requirements: + Call::Context 0 + ExtUtils::MakeMaker 0 + TOML-Tiny-0.20 + pathname: O/OA/OALDERS/TOML-Tiny-0.20.tar.gz + provides: + TOML::Tiny 0.20 + TOML::Tiny::Grammar 0.20 + TOML::Tiny::Parser 0.20 + TOML::Tiny::Tokenizer 0.20 + TOML::Tiny::Util 0.20 + TOML::Tiny::Writer 0.20 + requirements: + Carp 0 + Data::Dumper 0 + Encode 0 + Exporter 0 + ExtUtils::MakeMaker 0 + Math::BigInt 1.999718 + perl 5.018 + Task-Weaken-1.06 + pathname: E/ET/ETHER/Task-Weaken-1.06.tar.gz + provides: + Task::Weaken 1.06 + requirements: + Config 0 + ExtUtils::MakeMaker 0 + File::Spec 0 + Scalar::Util 1.14 + perl 5.006 + strict 0 + Term-Size-Any-0.002 + pathname: F/FE/FERREIRA/Term-Size-Any-0.002.tar.gz + provides: + Term::Size::Any 0.002 + requirements: + Devel::Hide 0 + ExtUtils::MakeMaker 0 + Module::Load::Conditional 0 + Term::Size::Perl 0 + Test::More 0 + Term-Size-Perl-0.031 + pathname: F/FE/FERREIRA/Term-Size-Perl-0.031.tar.gz + provides: + Term::Size::Perl 0.031 + requirements: + Exporter 0 + ExtUtils::CBuilder 0 + ExtUtils::MakeMaker 0 + Term-Table-0.024 + pathname: E/EX/EXODIST/Term-Table-0.024.tar.gz + provides: + Term::Table 0.024 + Term::Table::Cell 0.024 + Term::Table::CellStack 0.024 + Term::Table::HashBase 0.024 + Term::Table::LineBreak 0.024 + Term::Table::Spacer 0.024 + Term::Table::Util 0.024 + requirements: + Carp 0 + ExtUtils::MakeMaker 0 + List::Util 0 + Scalar::Util 0 + perl 5.008001 + Test-Abortable-0.003 + pathname: R/RJ/RJBS/Test-Abortable-0.003.tar.gz + provides: + Test::Abortable 0.003 + requirements: + ExtUtils::MakeMaker 6.78 + Sub::Exporter 0 + Test2::API 1.302075 + perl 5.012 + strict 0 + warnings 0 + Test-Deep-1.205 + pathname: R/RJ/RJBS/Test-Deep-1.205.tar.gz + provides: + Test::Deep 1.205 + Test::Deep::All 1.205 + Test::Deep::Any 1.205 + Test::Deep::Array 1.205 + Test::Deep::ArrayEach 1.205 + Test::Deep::ArrayElementsOnly 1.205 + Test::Deep::ArrayLength 1.205 + Test::Deep::ArrayLengthOnly 1.205 + Test::Deep::Blessed 1.205 + Test::Deep::Boolean 1.205 + Test::Deep::Cache 1.205 + Test::Deep::Cache::Simple 1.205 + Test::Deep::Class 1.205 + Test::Deep::Cmp 1.205 + Test::Deep::Code 1.205 + Test::Deep::Hash 1.205 + Test::Deep::HashEach 1.205 + Test::Deep::HashElements 1.205 + Test::Deep::HashKeys 1.205 + Test::Deep::HashKeysOnly 1.205 + Test::Deep::Ignore 1.205 + Test::Deep::Isa 1.205 + Test::Deep::ListMethods 1.205 + Test::Deep::MM 1.205 + Test::Deep::Methods 1.205 + Test::Deep::NoTest 1.205 + Test::Deep::None 1.205 + Test::Deep::Number 1.205 + Test::Deep::Obj 1.205 + Test::Deep::Ref 1.205 + Test::Deep::RefType 1.205 + Test::Deep::Regexp 1.205 + Test::Deep::RegexpMatches 1.205 + Test::Deep::RegexpOnly 1.205 + Test::Deep::RegexpRef 1.205 + Test::Deep::RegexpRefOnly 1.205 + Test::Deep::RegexpVersion 1.205 + Test::Deep::ScalarRef 1.205 + Test::Deep::ScalarRefOnly 1.205 + Test::Deep::Set 1.205 + Test::Deep::Shallow 1.205 + Test::Deep::Stack 1.205 + Test::Deep::String 1.205 + Test::Deep::SubHash 1.205 + Test::Deep::SubHashElements 1.205 + Test::Deep::SubHashKeys 1.205 + Test::Deep::SubHashKeysOnly 1.205 + Test::Deep::SuperHash 1.205 + Test::Deep::SuperHashElements 1.205 + Test::Deep::SuperHashKeys 1.205 + Test::Deep::SuperHashKeysOnly 1.205 + requirements: + ExtUtils::MakeMaker 6.78 + List::Util 1.09 + Scalar::Util 1.09 + Test::Builder 0 + Test::More 0.96 + perl 5.012 + Test-Differences-0.71 + pathname: D/DC/DCANTRELL/Test-Differences-0.71.tar.gz + provides: + Test::Differences 0.71 + requirements: + Capture::Tiny 0.24 + Data::Dumper 2.126 + ExtUtils::MakeMaker 0 + Test::More 0.88 + Text::Diff 1.43 + Test-Fatal-0.017 + pathname: R/RJ/RJBS/Test-Fatal-0.017.tar.gz + provides: + Test::Fatal 0.017 + requirements: + Carp 0 + Exporter 5.57 + ExtUtils::MakeMaker 6.78 + Test::Builder 0 + Try::Tiny 0.07 + strict 0 + warnings 0 + Test-Harness-3.50 + pathname: L/LE/LEONT/Test-Harness-3.50.tar.gz + provides: + App::Prove 3.50 + App::Prove::State 3.50 + App::Prove::State::Result 3.50 + App::Prove::State::Result::Test 3.50 + Harness::Hook undef + TAP::Base 3.50 + TAP::Formatter::Base 3.50 + TAP::Formatter::Color 3.50 + TAP::Formatter::Console 3.50 + TAP::Formatter::Console::ParallelSession 3.50 + TAP::Formatter::Console::Session 3.50 + TAP::Formatter::File 3.50 + TAP::Formatter::File::Session 3.50 + TAP::Formatter::Session 3.50 + TAP::Harness 3.50 + TAP::Harness::Env 3.50 + TAP::Object 3.50 + TAP::Parser 3.50 + TAP::Parser::Aggregator 3.50 + TAP::Parser::Grammar 3.50 + TAP::Parser::Iterator 3.50 + TAP::Parser::Iterator::Array 3.50 + TAP::Parser::Iterator::Process 3.50 + TAP::Parser::Iterator::Stream 3.50 + TAP::Parser::IteratorFactory 3.50 + TAP::Parser::Multiplexer 3.50 + TAP::Parser::Result 3.50 + TAP::Parser::Result::Bailout 3.50 + TAP::Parser::Result::Comment 3.50 + TAP::Parser::Result::Plan 3.50 + TAP::Parser::Result::Pragma 3.50 + TAP::Parser::Result::Test 3.50 + TAP::Parser::Result::Unknown 3.50 + TAP::Parser::Result::Version 3.50 + TAP::Parser::Result::YAML 3.50 + TAP::Parser::ResultFactory 3.50 + TAP::Parser::Scheduler 3.50 + TAP::Parser::Scheduler::Job 3.50 + TAP::Parser::Scheduler::Spinner 3.50 + TAP::Parser::Source 3.50 + TAP::Parser::SourceHandler 3.50 + TAP::Parser::SourceHandler::Executable 3.50 + TAP::Parser::SourceHandler::File 3.50 + TAP::Parser::SourceHandler::Handle 3.50 + TAP::Parser::SourceHandler::Perl 3.50 + TAP::Parser::SourceHandler::RawTAP 3.50 + TAP::Parser::YAMLish::Reader 3.50 + TAP::Parser::YAMLish::Writer 3.50 + Test::Harness 3.50 + requirements: + ExtUtils::MakeMaker 0 + Test-LongString-0.17 + pathname: R/RG/RGARCIA/Test-LongString-0.17.tar.gz + provides: + Test::LongString 0.17 + requirements: + ExtUtils::MakeMaker 0 + Test::Builder 0.12 + Test::Builder::Tester 1.04 + Test-MockRandom-1.01 + pathname: D/DA/DAGOLDEN/Test-MockRandom-1.01.tar.gz + provides: + Test::MockRandom 1.01 + requirements: + Carp 0 + ExtUtils::MakeMaker 6.17 + strict 0 + warnings 0 + Test-Perl-Critic-1.04 + pathname: P/PE/PETDANCE/Test-Perl-Critic-1.04.tar.gz + provides: + Test::Perl::Critic 1.04 + requirements: + Carp 0 + English 0 + MCE 1.827 + Module::Build 0.4 + Perl::Critic 1.105 + Perl::Critic::Utils 1.105 + Perl::Critic::Violation 1.105 + Test::Builder 0.88 + Test::More 0 + strict 0 + warnings 0 + Test-Requires-0.11 + pathname: T/TO/TOKUHIROM/Test-Requires-0.11.tar.gz + provides: + Test::Requires 0.11 + requirements: + ExtUtils::MakeMaker 6.64 + Test::Builder::Module 0 + Test::More 0.47 + perl 5.006 + Test-RequiresInternet-0.05 + pathname: M/MA/MALLEN/Test-RequiresInternet-0.05.tar.gz + provides: + Test::RequiresInternet 0.05 + requirements: + ExtUtils::MakeMaker 0 + Socket 0 + strict 0 + warnings 0 + Test-Routine-0.031 + pathname: R/RJ/RJBS/Test-Routine-0.031.tar.gz + provides: + Test::Routine 0.031 + Test::Routine::Common 0.031 + Test::Routine::Compositor 0.031 + Test::Routine::Manual::Demo 0.031 + Test::Routine::Runner 0.031 + Test::Routine::Test 0.031 + Test::Routine::Test::Role 0.031 + Test::Routine::Util 0.031 + requirements: + Carp 0 + Class::Load 0 + ExtUtils::MakeMaker 6.78 + Moose 0 + Moose::Exporter 0 + Moose::Meta::Class 0 + Moose::Meta::Method 0 + Moose::Role 0 + Moose::Util 0 + Moose::Util::TypeConstraints 0 + Params::Util 0 + Scalar::Util 0 + Sub::Exporter 0 + Sub::Exporter::Util 0 + Test2::API 1.302045 + Test::Abortable 0.002 + Test::More 0.96 + Try::Tiny 0 + namespace::autoclean 0 + namespace::clean 0 + perl 5.012000 + strict 0 + warnings 0 + Test-SharedFork-0.35 + pathname: E/EX/EXODIST/Test-SharedFork-0.35.tar.gz + provides: + Test::SharedFork 0.35 + Test::SharedFork::Array undef + Test::SharedFork::Scalar undef + Test::SharedFork::Store undef + requirements: + ExtUtils::MakeMaker 6.64 + File::Temp 0 + Test::Builder 0.32 + Test::Builder::Module 0 + Test::More 0.88 + perl 5.008_001 + Test-Simple-1.302210 + pathname: E/EX/EXODIST/Test-Simple-1.302210.tar.gz + provides: + Test2 1.302210 + Test2::API 1.302210 + Test2::API::Breakage 1.302210 + Test2::API::Context 1.302210 + Test2::API::Instance 1.302210 + Test2::API::InterceptResult 1.302210 + Test2::API::InterceptResult::Event 1.302210 + Test2::API::InterceptResult::Facet 1.302210 + Test2::API::InterceptResult::Hub 1.302210 + Test2::API::InterceptResult::Squasher 1.302210 + Test2::API::Stack 1.302210 + Test2::AsyncSubtest 1.302210 + Test2::AsyncSubtest::Event::Attach 1.302210 + Test2::AsyncSubtest::Event::Detach 1.302210 + Test2::AsyncSubtest::Formatter 1.302210 + Test2::AsyncSubtest::Hub 1.302210 + Test2::Bundle 1.302210 + Test2::Bundle::Extended 1.302210 + Test2::Bundle::More 1.302210 + Test2::Bundle::Simple 1.302210 + Test2::Compare 1.302210 + Test2::Compare::Array 1.302210 + Test2::Compare::Bag 1.302210 + Test2::Compare::Base 1.302210 + Test2::Compare::Bool 1.302210 + Test2::Compare::Custom 1.302210 + Test2::Compare::DeepRef 1.302210 + Test2::Compare::Delta 1.302210 + Test2::Compare::Event 1.302210 + Test2::Compare::EventMeta 1.302210 + Test2::Compare::Float 1.302210 + Test2::Compare::Hash 1.302210 + Test2::Compare::Isa 1.302210 + Test2::Compare::Meta 1.302210 + Test2::Compare::Negatable 1.302210 + Test2::Compare::Number 1.302210 + Test2::Compare::Object 1.302210 + Test2::Compare::OrderedSubset 1.302210 + Test2::Compare::Pattern 1.302210 + Test2::Compare::Ref 1.302210 + Test2::Compare::Regex 1.302210 + Test2::Compare::Scalar 1.302210 + Test2::Compare::Set 1.302210 + Test2::Compare::String 1.302210 + Test2::Compare::Undef 1.302210 + Test2::Compare::Wildcard 1.302210 + Test2::Env 1.302210 + Test2::Event 1.302210 + Test2::Event::Bail 1.302210 + Test2::Event::Diag 1.302210 + Test2::Event::Encoding 1.302210 + Test2::Event::Exception 1.302210 + Test2::Event::Fail 1.302210 + Test2::Event::Generic 1.302210 + Test2::Event::Note 1.302210 + Test2::Event::Ok 1.302210 + Test2::Event::Pass 1.302210 + Test2::Event::Plan 1.302210 + Test2::Event::Skip 1.302210 + Test2::Event::Subtest 1.302210 + Test2::Event::TAP::Version 1.302210 + Test2::Event::V2 1.302210 + Test2::Event::Waiting 1.302210 + Test2::EventFacet 1.302210 + Test2::EventFacet::About 1.302210 + Test2::EventFacet::Amnesty 1.302210 + Test2::EventFacet::Assert 1.302210 + Test2::EventFacet::Control 1.302210 + Test2::EventFacet::Error 1.302210 + Test2::EventFacet::Hub 1.302210 + Test2::EventFacet::Info 1.302210 + Test2::EventFacet::Info::Table 1.302210 + Test2::EventFacet::Meta 1.302210 + Test2::EventFacet::Parent 1.302210 + Test2::EventFacet::Plan 1.302210 + Test2::EventFacet::Render 1.302210 + Test2::EventFacet::Trace 1.302210 + Test2::Formatter 1.302210 + Test2::Formatter::TAP 1.302210 + Test2::Hub 1.302210 + Test2::Hub::Interceptor 1.302210 + Test2::Hub::Interceptor::Terminator 1.302210 + Test2::Hub::Subtest 1.302210 + Test2::IPC 1.302210 + Test2::IPC::Driver 1.302210 + Test2::IPC::Driver::Files 1.302210 + Test2::Manual 1.302210 + Test2::Manual::Anatomy 1.302210 + Test2::Manual::Anatomy::API 1.302210 + Test2::Manual::Anatomy::Context 1.302210 + Test2::Manual::Anatomy::EndToEnd 1.302210 + Test2::Manual::Anatomy::Event 1.302210 + Test2::Manual::Anatomy::Hubs 1.302210 + Test2::Manual::Anatomy::IPC 1.302210 + Test2::Manual::Anatomy::Utilities 1.302210 + Test2::Manual::Concurrency 1.302210 + Test2::Manual::Contributing 1.302210 + Test2::Manual::Testing 1.302210 + Test2::Manual::Testing::Introduction 1.302210 + Test2::Manual::Testing::Migrating 1.302210 + Test2::Manual::Testing::Planning 1.302210 + Test2::Manual::Testing::Todo 1.302210 + Test2::Manual::Tooling 1.302210 + Test2::Manual::Tooling::FirstTool 1.302210 + Test2::Manual::Tooling::Formatter 1.302210 + Test2::Manual::Tooling::Nesting 1.302210 + Test2::Manual::Tooling::Plugin::TestExit 1.302210 + Test2::Manual::Tooling::Plugin::TestingDone 1.302210 + Test2::Manual::Tooling::Plugin::ToolCompletes 1.302210 + Test2::Manual::Tooling::Plugin::ToolStarts 1.302210 + Test2::Manual::Tooling::Subtest 1.302210 + Test2::Manual::Tooling::TestBuilder 1.302210 + Test2::Manual::Tooling::Testing 1.302210 + Test2::Mock 1.302210 + Test2::Plugin 1.302210 + Test2::Plugin::BailOnFail 1.302210 + Test2::Plugin::DieOnFail 1.302210 + Test2::Plugin::ExitSummary 1.302210 + Test2::Plugin::SRand 1.302210 + Test2::Plugin::Times 1.302210 + Test2::Plugin::UTF8 1.302210 + Test2::Require 1.302210 + Test2::Require::AuthorTesting 1.302210 + Test2::Require::AutomatedTesting 1.302210 + Test2::Require::EnvVar 1.302210 + Test2::Require::ExtendedTesting 1.302210 + Test2::Require::Fork 1.302210 + Test2::Require::Module 1.302210 + Test2::Require::NonInteractiveTesting 1.302210 + Test2::Require::Perl 1.302210 + Test2::Require::RealFork 1.302210 + Test2::Require::ReleaseTesting 1.302210 + Test2::Require::Threads 1.302210 + Test2::Suite 1.302210 + Test2::Todo 1.302210 + Test2::Tools 1.302210 + Test2::Tools::AsyncSubtest 1.302210 + Test2::Tools::Basic 1.302210 + Test2::Tools::Class 1.302210 + Test2::Tools::ClassicCompare 1.302210 + Test2::Tools::Compare 1.302210 + Test2::Tools::Defer 1.302210 + Test2::Tools::Encoding 1.302210 + Test2::Tools::Event 1.302210 + Test2::Tools::Exception 1.302210 + Test2::Tools::Exports 1.302210 + Test2::Tools::GenTemp 1.302210 + Test2::Tools::Grab 1.302210 + Test2::Tools::Mock 1.302210 + Test2::Tools::Ref 1.302210 + Test2::Tools::Refcount 1.302210 + Test2::Tools::Spec 1.302210 + Test2::Tools::Subtest 1.302210 + Test2::Tools::Target 1.302210 + Test2::Tools::Tester 1.302210 + Test2::Tools::Tiny 1.302210 + Test2::Tools::Warnings 1.302210 + Test2::Util 1.302210 + Test2::Util::ExternalMeta 1.302210 + Test2::Util::Facets2Legacy 1.302210 + Test2::Util::Grabber 1.302210 + Test2::Util::Guard 1.302210 + Test2::Util::HashBase 1.302210 + Test2::Util::Importer 1.302210 + Test2::Util::Ref 1.302210 + Test2::Util::Sig 1.302210 + Test2::Util::Stash 1.302210 + Test2::Util::Sub 1.302210 + Test2::Util::Table 1.302210 + Test2::Util::Table::Cell 1.302210 + Test2::Util::Table::LineBreak 1.302210 + Test2::Util::Term 1.302210 + Test2::Util::Times 1.302210 + Test2::Util::Trace 1.302210 + Test2::V0 1.302210 + Test2::Workflow 1.302210 + Test2::Workflow::BlockBase 1.302210 + Test2::Workflow::Build 1.302210 + Test2::Workflow::Runner 1.302210 + Test2::Workflow::Task 1.302210 + Test2::Workflow::Task::Action 1.302210 + Test2::Workflow::Task::Group 1.302210 + Test::Builder 1.302210 + Test::Builder::Formatter 1.302210 + Test::Builder::Module 1.302210 + Test::Builder::Tester 1.302210 + Test::Builder::Tester::Color 1.302210 + Test::Builder::Tester::Tie 1.302210 + Test::Builder::TodoDiag 1.302210 + Test::More 1.302210 + Test::Simple 1.302210 + Test::Tester 1.302210 + Test::Tester::Capture 1.302210 + Test::Tester::CaptureRunner 1.302210 + Test::Tester::Delegate 1.302210 + Test::use::ok 1.302210 + ok 1.302210 + requirements: + B 0 + Data::Dumper 0 + Exporter 0 + ExtUtils::MakeMaker 0 + File::Spec 0 + File::Temp 0 + Scalar::Util 1.13 + Storable 0 + Term::Table 0.013 + Time::HiRes 0 + overload 0 + perl 5.006002 + utf8 0 + Test-TCP-2.22 + pathname: M/MI/MIYAGAWA/Test-TCP-2.22.tar.gz + provides: + Net::EmptyPort undef + Test::TCP 2.22 + Test::TCP::CheckPort undef + requirements: + ExtUtils::MakeMaker 6.64 + IO::Socket::INET 0 + IO::Socket::IP 0 + Test::More 0 + Test::SharedFork 0.29 + Time::HiRes 0 + perl 5.008001 + Test-Vars-0.017 + pathname: J/JK/JKEENAN/Test-Vars-0.017.tar.gz + provides: + Test::Vars 0.017 + requirements: + B 0 + ExtUtils::MakeMaker 6.17 + ExtUtils::Manifest 0 + IO::Pipe 0 + List::Util 1.33 + Storable 0 + Symbol 0 + parent 0 + perl 5.010000 + Test-Warn-0.37 + pathname: B/BI/BIGJ/Test-Warn-0.37.tar.gz + provides: + Test::Warn 0.37 + requirements: + Carp 1.22 + ExtUtils::MakeMaker 0 + Sub::Uplevel 0.12 + Test::Builder 0.13 + Test::Builder::Tester 1.02 + perl 5.006 + Text-CSV_XS-1.60 + pathname: H/HM/HMBRAND/Text-CSV_XS-1.60.tgz + provides: + Text::CSV_XS 1.60 + requirements: + Config 0 + ExtUtils::MakeMaker 0 + IO::Handle 0 + Test::More 0 + XSLoader 0 + Text-Diff-1.45 + pathname: N/NE/NEILB/Text-Diff-1.45.tar.gz + provides: + Text::Diff 1.45 + Text::Diff::Base 1.45 + Text::Diff::Config 1.44 + Text::Diff::Table 1.44 + requirements: + Algorithm::Diff 1.19 + Exporter 0 + ExtUtils::MakeMaker 0 + perl 5.006 + Text-Glob-0.11 + pathname: R/RC/RCLAMP/Text-Glob-0.11.tar.gz + provides: + Text::Glob 0.11 + requirements: + Exporter 0 + ExtUtils::MakeMaker 0 + constant 0 + perl 5.00503 + Text-SimpleTable-2.07 + pathname: M/MR/MRAMBERG/Text-SimpleTable-2.07.tar.gz + provides: + Text::SimpleTable 2.07 + requirements: + ExtUtils::MakeMaker 0 + Test::More 0 + perl 5.008001 + Text-SimpleTable-AutoWidth-0.09 + pathname: C/CU/CUB/Text-SimpleTable-AutoWidth-0.09.tar.gz + provides: + Text::SimpleTable::AutoWidth 0.09 + requirements: + ExtUtils::MakeMaker 0 + List::Util 0 + Moo 0 + Text::SimpleTable 0 + strict 0 + warnings 0 + Text-Template-1.61 + pathname: M/MS/MSCHOUT/Text-Template-1.61.tar.gz + provides: + Text::Template 1.61 + Text::Template::Preprocess 1.61 + requirements: + Carp 0 + Encode 0 + Exporter 0 + ExtUtils::MakeMaker 0 + base 0 + perl 5.008 + strict 0 + warnings 0 + Throwable-1.001 + pathname: R/RJ/RJBS/Throwable-1.001.tar.gz + provides: + StackTrace::Auto 1.001 + Throwable 1.001 + Throwable::Error 1.001 + requirements: + Carp 0 + Devel::StackTrace 1.32 + ExtUtils::MakeMaker 6.78 + Module::Runtime 0.002 + Moo 1.000001 + Moo::Role 0 + Scalar::Util 0 + Sub::Quote 0 + overload 0 + Tie-ToObject-0.03 + pathname: N/NU/NUFFIN/Tie-ToObject-0.03.tar.gz + provides: + Tie::ToObject 0.03 + requirements: + ExtUtils::MakeMaker 0 + Scalar::Util 0 + Test::More 0 + Test::use::ok 0 + Tie::RefHash 0 + Time-Duration-1.21 + pathname: N/NE/NEILB/Time-Duration-1.21.tar.gz + provides: + Time::Duration 1.21 + requirements: + Exporter 0 + ExtUtils::MakeMaker 0 + constant 0 + perl 5.006 + strict 0 + warnings 0 + Time-Duration-Parse-0.16 + pathname: N/NE/NEILB/Time-Duration-Parse-0.16.tar.gz + provides: + Time::Duration::Parse 0.16 + requirements: + Carp 0 + Exporter 5.57 + ExtUtils::MakeMaker 0 + perl 5.006 + strict 0 + warnings 0 + Time-Local-1.35 + pathname: D/DR/DROLSKY/Time-Local-1.35.tar.gz + provides: + Time::Local 1.35 + requirements: + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 0 + constant 0 + parent 0 + strict 0 + TimeDate-2.33 + pathname: A/AT/ATOOMIC/TimeDate-2.33.tar.gz + provides: + Date::Format 2.24 + Date::Format::Generic 2.24 + Date::Language 1.10 + Date::Language::Afar 0.99 + Date::Language::Amharic 1.00 + Date::Language::Austrian 1.01 + Date::Language::Brazilian 1.01 + Date::Language::Bulgarian 1.01 + Date::Language::Chinese 1.00 + Date::Language::Chinese_GB 1.01 + Date::Language::Czech 1.01 + Date::Language::Danish 1.01 + Date::Language::Dutch 1.02 + Date::Language::English 1.01 + Date::Language::Finnish 1.01 + Date::Language::French 1.04 + Date::Language::Gedeo 0.99 + Date::Language::German 1.02 + Date::Language::Greek 1.00 + Date::Language::Hungarian 1.01 + Date::Language::Icelandic 1.01 + Date::Language::Italian 1.01 + Date::Language::Norwegian 1.01 + Date::Language::Occitan 1.04 + Date::Language::Oromo 0.99 + Date::Language::Romanian 1.01 + Date::Language::Russian 1.01 + Date::Language::Russian_cp1251 1.01 + Date::Language::Russian_koi8r 1.01 + Date::Language::Sidama 0.99 + Date::Language::Somali 0.99 + Date::Language::Spanish 1.00 + Date::Language::Swedish 1.01 + Date::Language::Tigrinya 1.00 + Date::Language::TigrinyaEritrean 1.00 + Date::Language::TigrinyaEthiopian 1.00 + Date::Language::Turkish 1.0 + Date::Parse 2.33 + Time::Zone 2.24 + TimeDate 1.21 + requirements: + ExtUtils::MakeMaker 0 + Tree-Simple-1.34 + pathname: R/RS/RSAVAGE/Tree-Simple-1.34.tgz + provides: + Tree::Simple 1.34 + Tree::Simple::Visitor 1.34 + requirements: + ExtUtils::MakeMaker 0 + Scalar::Util 1.18 + constant 0 + strict 0 + warnings 0 + Tree-Simple-VisitorFactory-0.16 + pathname: R/RS/RSAVAGE/Tree-Simple-VisitorFactory-0.16.tgz + provides: + Tree::Simple::Visitor::BreadthFirstTraversal 0.16 + Tree::Simple::Visitor::CreateDirectoryTree 0.16 + Tree::Simple::Visitor::FindByNodeValue 0.16 + Tree::Simple::Visitor::FindByPath 0.16 + Tree::Simple::Visitor::FindByUID 0.16 + Tree::Simple::Visitor::FromNestedArray 0.16 + Tree::Simple::Visitor::FromNestedHash 0.16 + Tree::Simple::Visitor::GetAllDescendents 0.16 + Tree::Simple::Visitor::LoadClassHierarchy 0.16 + Tree::Simple::Visitor::LoadDirectoryTree 0.16 + Tree::Simple::Visitor::PathToRoot 0.16 + Tree::Simple::Visitor::PostOrderTraversal 0.16 + Tree::Simple::Visitor::PreOrderTraversal 0.16 + Tree::Simple::Visitor::Sort 0.16 + Tree::Simple::Visitor::ToNestedArray 0.16 + Tree::Simple::Visitor::ToNestedHash 0.16 + Tree::Simple::Visitor::VariableDepthClone 0.16 + Tree::Simple::VisitorFactory 0.16 + requirements: + ExtUtils::MakeMaker 0 + File::Spec 0.6 + Scalar::Util 1.1 + Tree::Simple 1.12 + Tree::Simple::Visitor 1.22 + base 0 + strict 0 + warnings 0 + Try-Tiny-0.32 + pathname: E/ET/ETHER/Try-Tiny-0.32.tar.gz + provides: + Try::Tiny 0.32 + requirements: + Carp 0 + Exporter 5.57 + ExtUtils::MakeMaker 0 + constant 0 + perl 5.006 + strict 0 + warnings 0 + Twitter-API-1.0006 + pathname: M/MM/MMIMS/Twitter-API-1.0006.tar.gz + provides: + Twitter::API 1.0006 + Twitter::API::Context 1.0006 + Twitter::API::Error 1.0006 + Twitter::API::Role::RequestArgs 1.0006 + Twitter::API::Trait::ApiMethods 1.0006 + Twitter::API::Trait::AppAuth 1.0006 + Twitter::API::Trait::DecodeHtmlEntities 1.0006 + Twitter::API::Trait::Enchilada 1.0006 + Twitter::API::Trait::Migration 1.0006 + Twitter::API::Trait::NormalizeBooleans 1.0006 + Twitter::API::Trait::RateLimiting 1.0006 + Twitter::API::Trait::RetryOnError 1.0006 + Twitter::API::Util 1.0006 + requirements: + Carp 0 + Digest::SHA 0 + Encode 0 + HTML::Entities 0 + HTTP::Request::Common 0 + HTTP::Thin 0 + IO::Socket::SSL 0 + JSON::MaybeXS 0 + Module::Build::Tiny 0.034 + Module::Runtime 0 + Moo 0 + Moo::Role 0 + MooX::Aliases 0 + MooX::Traits 0 + Ref::Util 0 + Scalar::Util 0 + StackTrace::Auto 0 + Sub::Exporter::Progressive 0 + Throwable 0 + Time::HiRes 0 + Time::Local 0 + Try::Tiny 0 + URI 0 + URL::Encode 0 + WWW::OAuth 0.006 + namespace::clean 0 + perl v5.14.1 + Type-Tiny-2.008001 + pathname: T/TO/TOBYINK/Type-Tiny-2.008001.tar.gz + provides: + Devel::TypeTiny::Perl58Compat 2.008001 + Error::TypeTiny 2.008001 + Error::TypeTiny::Assertion 2.008001 + Error::TypeTiny::Compilation 2.008001 + Error::TypeTiny::WrongNumberOfParameters 2.008001 + Eval::TypeTiny 2.008001 + Eval::TypeTiny::CodeAccumulator 2.008001 + Reply::Plugin::TypeTiny 2.008001 + Test::TypeTiny 2.008001 + Type::Coercion 2.008001 + Type::Coercion::FromMoose 2.008001 + Type::Coercion::Union 2.008001 + Type::Library 2.008001 + Type::Params 2.008001 + Type::Params::Alternatives 2.008001 + Type::Params::Parameter 2.008001 + Type::Params::Signature 2.008001 + Type::Parser 2.008001 + Type::Parser::AstBuilder 2.008001 + Type::Parser::Token 2.008001 + Type::Parser::TokenStream 2.008001 + Type::Registry 2.008001 + Type::Tie 2.008001 + Type::Tie::ARRAY 2.008001 + Type::Tie::BASE 2.008001 + Type::Tie::HASH 2.008001 + Type::Tie::SCALAR 2.008001 + Type::Tiny 2.008001 + Type::Tiny::Bitfield 2.008001 + Type::Tiny::Class 2.008001 + Type::Tiny::ConstrainedObject 2.008001 + Type::Tiny::Duck 2.008001 + Type::Tiny::Enum 2.008001 + Type::Tiny::Intersection 2.008001 + Type::Tiny::Role 2.008001 + Type::Tiny::Union 2.008001 + Type::Utils 2.008001 + Types::Common 2.008001 + Types::Common::Numeric 2.008001 + Types::Common::String 2.008001 + Types::Standard 2.008001 + Types::Standard::ArrayRef 2.008001 + Types::Standard::CycleTuple 2.008001 + Types::Standard::Dict 2.008001 + Types::Standard::HashRef 2.008001 + Types::Standard::Map 2.008001 + Types::Standard::ScalarRef 2.008001 + Types::Standard::StrMatch 2.008001 + Types::Standard::Tied 2.008001 + Types::Standard::Tuple 2.008001 + Types::TypeTiny 2.008001 + requirements: + Exporter::Tiny 1.006000 + ExtUtils::MakeMaker 6.17 + perl 5.008001 + Types-Path-Tiny-0.006 + pathname: D/DA/DAGOLDEN/Types-Path-Tiny-0.006.tar.gz + provides: + Types::Path::Tiny 0.006 + requirements: + ExtUtils::MakeMaker 6.17 + Path::Tiny 0 + Type::Library 0.008 + Type::Utils 0 + Types::Standard 0 + Types::TypeTiny 0.004 + perl 5.008001 + strict 0 + warnings 0 + Types-Self-0.002 + pathname: T/TO/TOBYINK/Types-Self-0.002.tar.gz + provides: + Types::Self 0.002 + requirements: + ExtUtils::MakeMaker 6.17 + Role::Hooks 0 + Types::Standard 1.012 + perl 5.008001 + Types-Serialiser-1.01 + pathname: M/ML/MLEHMANN/Types-Serialiser-1.01.tar.gz + provides: + JSON::PP::Boolean 1.01 + Types::Serialiser 1.01 + Types::Serialiser::BooleanBase 1.01 + Types::Serialiser::Error 1.01 + requirements: + ExtUtils::MakeMaker 0 + common::sense 0 + Types-URI-0.007 + pathname: T/TO/TOBYINK/Types-URI-0.007.tar.gz + provides: + Types::URI 0.007 + requirements: + ExtUtils::MakeMaker 6.17 + Type::Library 1.000000 + Types::Path::Tiny 0 + Types::Standard 0 + Types::UUID 0 + URI 0 + URI::FromHash 0 + perl 5.008 + Types-UUID-0.004 + pathname: T/TO/TOBYINK/Types-UUID-0.004.tar.gz + provides: + Types::UUID 0.004 + requirements: + ExtUtils::MakeMaker 6.17 + Type::Tiny 1.000000 + UUID::Tiny 1.02 + perl 5.008 + UNIVERSAL-require-0.19 + pathname: N/NE/NEILB/UNIVERSAL-require-0.19.tar.gz + provides: + UNIVERSAL::require 0.19 + requirements: + Carp 0 + ExtUtils::MakeMaker 0 + Test::More 0.47 + perl 5.006 + strict 0 + warnings 0 + URI-5.31 + pathname: O/OA/OALDERS/URI-5.31.tar.gz + provides: + URI 5.31 + URI::Escape 5.31 + URI::Heuristic 5.31 + URI::IRI 5.31 + URI::QueryParam 5.31 + URI::Split 5.31 + URI::URL 5.31 + URI::WithBase 5.31 + URI::data 5.31 + URI::file 5.31 + URI::file::Base 5.31 + URI::file::FAT 5.31 + URI::file::Mac 5.31 + URI::file::OS2 5.31 + URI::file::QNX 5.31 + URI::file::Unix 5.31 + URI::file::Win32 5.31 + URI::ftp 5.31 + URI::ftpes 5.31 + URI::ftps 5.31 + URI::geo 5.31 + URI::gopher 5.31 + URI::http 5.31 + URI::https 5.31 + URI::icap 5.31 + URI::icaps 5.31 + URI::irc 5.31 + URI::ircs 5.31 + URI::ldap 5.31 + URI::ldapi 5.31 + URI::ldaps 5.31 + URI::mailto 5.31 + URI::mms 5.31 + URI::news 5.31 + URI::nntp 5.31 + URI::nntps 5.31 + URI::otpauth 5.31 + URI::pop 5.31 + URI::rlogin 5.31 + URI::rsync 5.31 + URI::rtsp 5.31 + URI::rtspu 5.31 + URI::scp 5.31 + URI::sftp 5.31 + URI::sip 5.31 + URI::sips 5.31 + URI::snews 5.31 + URI::ssh 5.31 + URI::telnet 5.31 + URI::tn3270 5.31 + URI::urn 5.31 + URI::urn::isbn 5.31 + URI::urn::oid 5.31 + requirements: + Carp 0 + Cwd 0 + Data::Dumper 0 + Encode 0 + Exporter 5.57 + ExtUtils::MakeMaker 0 + MIME::Base32 0 + MIME::Base64 2 + Net::Domain 0 + Scalar::Util 0 + constant 0 + integer 0 + overload 0 + parent 0 + perl 5.008001 + strict 0 + utf8 0 + warnings 0 + URI-Find-20160806 + pathname: M/MS/MSCHWERN/URI-Find-20160806.tar.gz + provides: + URI::Find 20160806 + URI::Find::Schemeless 20160806 + requirements: + Module::Build 0.30 + Test::More 0.88 + URI 1.60 + perl v5.8.8 + URI-FromHash-0.05 + pathname: D/DR/DROLSKY/URI-FromHash-0.05.tar.gz + provides: + URI::FromHash 0.05 + requirements: + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 0 + Params::Validate 0 + URI 1.68 + strict 0 + warnings 0 + URI-Nested-0.10 + pathname: D/DW/DWHEELER/URI-Nested-0.10.tar.gz + provides: + URI::Nested 0.10 + requirements: + Module::Build 0.30 + Test::More 0.88 + URI 1.40 + perl 5.008001 + URI-db-0.23 + pathname: D/DW/DWHEELER/URI-db-0.23.tar.gz + provides: + URI::cassandra 0.23 + URI::clickhouse 0.20 + URI::cockroach 0.23 + URI::cockroachdb 0.23 + URI::couch 0.23 + URI::couchdb 0.23 + URI::cubrid 0.23 + URI::db 0.23 + URI::db2 0.23 + URI::derby 0.23 + URI::exasol 0.23 + URI::firebird 0.23 + URI::hive 0.23 + URI::impala 0.23 + URI::informix 0.23 + URI::ingres 0.23 + URI::interbase 0.23 + URI::ldapdb 0.23 + URI::maria 0.23 + URI::mariadb 0.23 + URI::max 0.23 + URI::maxdb 0.23 + URI::monet 0.23 + URI::monetdb 0.23 + URI::mongo 0.23 + URI::mongodb 0.23 + URI::mssql 0.23 + URI::mysql 0.23 + URI::oracle 0.23 + URI::pg 0.23 + URI::pgsql 0.23 + URI::pgxc 0.23 + URI::postgres 0.23 + URI::postgresql 0.23 + URI::postgresxc 0.23 + URI::redshift 0.23 + URI::snowflake 0.23 + URI::sqlite 0.23 + URI::sqlite3 0.23 + URI::sqlserver 0.23 + URI::sybase 0.23 + URI::teradata 0.23 + URI::unify 0.23 + URI::vertica 0.23 + URI::yugabyte 0.23 + URI::yugabytedb 0.23 + requirements: + Module::Build 0.30 + Test::More 0.88 + URI 1.40 + URI::Nested 0.10 + perl 5.008001 + URI-ws-0.03 + pathname: P/PL/PLICEASE/URI-ws-0.03.tar.gz + provides: + URI::ws 0.03 + URI::wss 0.03 + requirements: + ExtUtils::MakeMaker 6.30 + URI 0 + URL-Encode-0.03 + pathname: C/CH/CHANSEN/URL-Encode-0.03.tar.gz + provides: + URL::Encode 0.03 + URL::Encode::PP 0.03 + requirements: + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 6.59 + Test::More 0.88 + XSLoader 0 + perl 5.008001 + UUID-Tiny-1.04 + pathname: C/CA/CAUGUSTIN/UUID-Tiny-1.04.tar.gz + provides: + UUID::Tiny 1.04 + requirements: + Carp 0 + Digest::MD5 0 + ExtUtils::MakeMaker 0 + IO::File 0 + MIME::Base64 0 + POSIX 0 + Test::More 0 + Time::HiRes 0 + Unicode-LineBreak-2019.001 + pathname: N/NE/NEZUMI/Unicode-LineBreak-2019.001.tar.gz + provides: + Text::LineFold 2018.012 + Unicode::GCString 2013.10 + Unicode::LineBreak 2019.001 + requirements: + Encode 1.98 + ExtUtils::MakeMaker 6.26 + MIME::Charset v1.6.2 + Test::More 0.45 + perl 5.008 + Variable-Magic-0.64 + pathname: V/VP/VPIT/Variable-Magic-0.64.tar.gz + provides: + Variable::Magic 0.64 + requirements: + Carp 0 + Config 0 + Exporter 0 + ExtUtils::MakeMaker 0 + IO::Handle 0 + IO::Select 0 + IPC::Open3 0 + POSIX 0 + Socket 0 + Test::More 0 + XSLoader 0 + base 0 + lib 0 + perl 5.008 + WWW-Form-UrlEncoded-0.26 + pathname: K/KA/KAZEBURO/WWW-Form-UrlEncoded-0.26.tar.gz + provides: + WWW::Form::UrlEncoded 0.26 + WWW::Form::UrlEncoded::PP undef + requirements: + Exporter 0 + Module::Build 0.4005 + perl 5.008001 + WWW-OAuth-1.003 + pathname: D/DB/DBOOK/WWW-OAuth-1.003.tar.gz + provides: + WWW::OAuth 1.003 + WWW::OAuth::Request 1.003 + WWW::OAuth::Request::Basic 1.003 + WWW::OAuth::Request::HTTP_Request 1.003 + WWW::OAuth::Request::Mojo 1.003 + WWW::OAuth::Util 1.003 + requirements: + Carp 0 + Class::Tiny::Chained 0 + Crypt::SysRandom 0 + Digest::SHA 0 + ExtUtils::MakeMaker 0 + List::Util 1.33 + Module::Runtime 0 + Role::Tiny 2.000000 + Scalar::Util 0 + URI 1.28 + URI::Escape 3.26 + WWW::Form::UrlEncoded 0.23 + perl 5.008001 + WWW-RobotRules-6.02 + pathname: G/GA/GAAS/WWW-RobotRules-6.02.tar.gz + provides: + WWW::RobotRules 6.02 + WWW::RobotRules::AnyDBM_File 6.00 + WWW::RobotRules::InCore 6.02 + requirements: + AnyDBM_File 0 + ExtUtils::MakeMaker 0 + Fcntl 0 + URI 1.10 + perl 5.008001 + XML-Parser-2.47 + pathname: T/TO/TODDR/XML-Parser-2.47.tar.gz + provides: + XML::Parser 2.47 + XML::Parser::Expat 2.47 + XML::Parser::Style::Debug undef + XML::Parser::Style::Objects undef + XML::Parser::Style::Stream undef + XML::Parser::Style::Subs undef + XML::Parser::Style::Tree undef + requirements: + ExtUtils::MakeMaker 0 + LWP::UserAgent 0 + perl 5.00405 + XML-XPath-1.48 + pathname: M/MA/MANWAR/XML-XPath-1.48.tar.gz + provides: + XML::XPath 1.48 + XML::XPath::Boolean 1.48 + XML::XPath::Builder 1.48 + XML::XPath::Expr 1.48 + XML::XPath::Function 1.48 + XML::XPath::Literal 1.48 + XML::XPath::LocationPath 1.48 + XML::XPath::Node 1.48 + XML::XPath::Node::Attribute 1.48 + XML::XPath::Node::AttributeImpl 1.48 + XML::XPath::Node::Comment 1.48 + XML::XPath::Node::Element 1.48 + XML::XPath::Node::Namespace 1.48 + XML::XPath::Node::PI 1.48 + XML::XPath::Node::Text 1.48 + XML::XPath::NodeSet 1.48 + XML::XPath::Number 1.48 + XML::XPath::Parser 1.48 + XML::XPath::PerlSAX 1.48 + XML::XPath::Root 1.48 + XML::XPath::Step 1.48 + XML::XPath::Variable 1.48 + XML::XPath::XMLParser 1.48 + requirements: + ExtUtils::MakeMaker 0 + Path::Tiny 0.076 + Scalar::Util 1.45 + Test::More 0 + XML::Parser 2.23 + perl 5.010001 + XSLoader-0.24 + pathname: S/SA/SAPER/XSLoader-0.24.tar.gz + provides: + XSLoader 0.24 + requirements: + ExtUtils::MakeMaker 0 + Test::More 0.47 + XString-0.005 + pathname: A/AT/ATOOMIC/XString-0.005.tar.gz + provides: + XString 0.005 + requirements: + ExtUtils::MakeMaker 0 + perl 5.008 + YAML-1.31 + pathname: I/IN/INGY/YAML-1.31.tar.gz + provides: + YAML 1.31 + YAML::Any 1.31 + YAML::Dumper undef + YAML::Dumper::Base undef + YAML::Error undef + YAML::Loader undef + YAML::Loader::Base undef + YAML::Marshall undef + YAML::Mo undef + YAML::Node undef + YAML::Tag undef + YAML::Type::blessed undef + YAML::Type::code undef + YAML::Type::glob undef + YAML::Type::ref undef + YAML::Type::regexp undef + YAML::Type::undef undef + YAML::Types undef + YAML::Warning undef + yaml_mapping undef + yaml_scalar undef + yaml_sequence undef + requirements: + ExtUtils::MakeMaker 0 + perl 5.008001 + YAML-LibYAML-v0.903.0 + pathname: T/TI/TINITA/YAML-LibYAML-v0.903.0.tar.gz + provides: + YAML::LibYAML v0.903.0 + YAML::XS v0.903.0 + requirements: + B::Deparse 0 + Exporter 0 + ExtUtils::MakeMaker 0 + Scalar::Util 0 + base 0 + constant 0 + perl 5.008001 + strict 0 + warnings 0 + YAML-PP-v0.39.0 + pathname: T/TI/TINITA/YAML-PP-v0.39.0.tar.gz + provides: + YAML::PP v0.39.0 + YAML::PP::Common v0.39.0 + YAML::PP::Constructor v0.39.0 + YAML::PP::Dumper v0.39.0 + YAML::PP::Emitter v0.39.0 + YAML::PP::Exception v0.39.0 + YAML::PP::Grammar v0.39.0 + YAML::PP::Highlight v0.39.0 + YAML::PP::Lexer v0.39.0 + YAML::PP::Loader v0.39.0 + YAML::PP::Parser v0.39.0 + YAML::PP::Perl v0.39.0 + YAML::PP::Preserve::Array v0.39.0 + YAML::PP::Preserve::Hash v0.39.0 + YAML::PP::Preserve::Scalar v0.39.0 + YAML::PP::Reader v0.39.0 + YAML::PP::Reader::File v0.39.0 + YAML::PP::Render v0.39.0 + YAML::PP::Representer v0.39.0 + YAML::PP::Schema v0.39.0 + YAML::PP::Schema::Binary v0.39.0 + YAML::PP::Schema::Catchall v0.39.0 + YAML::PP::Schema::Core v0.39.0 + YAML::PP::Schema::Failsafe v0.39.0 + YAML::PP::Schema::Include v0.39.0 + YAML::PP::Schema::JSON v0.39.0 + YAML::PP::Schema::Merge v0.39.0 + YAML::PP::Schema::Perl v0.39.0 + YAML::PP::Schema::Tie::IxHash v0.39.0 + YAML::PP::Schema::YAML1_1 v0.39.0 + YAML::PP::Type::MergeKey v0.39.0 + YAML::PP::Writer v0.39.0 + YAML::PP::Writer::File v0.39.0 + requirements: + B 0 + B::Deparse 0 + Carp 0 + Data::Dumper 0 + Encode 0 + Exporter 0 + ExtUtils::MakeMaker 0 + File::Basename 0 + Getopt::Long 0 + MIME::Base64 0 + Module::Load 0 + Scalar::Util 1.07 + Tie::Array 0 + Tie::Hash 0 + base 0 + constant 0 + overload 0 + perl 5.008000 + strict 0 + warnings 0 + bareword-filehandles-0.007 + pathname: I/IL/ILMARI/bareword-filehandles-0.007.tar.gz + provides: + bareword::filehandles 0.007 + requirements: + B::Hooks::OP::Check 0 + ExtUtils::Depends 0 + ExtUtils::MakeMaker 0 + Test::More 0.88 + XSLoader 0 + if 0 + perl 5.008001 + strict 0 + warnings 0 + common-sense-3.75 + pathname: M/ML/MLEHMANN/common-sense-3.75.tar.gz + provides: + common::sense 3.75 + requirements: + ExtUtils::MakeMaker 0 + indirect-0.39 + pathname: V/VP/VPIT/indirect-0.39.tar.gz + provides: + indirect 0.39 + requirements: + Carp 0 + Config 0 + ExtUtils::MakeMaker 0 + File::Spec 0 + IO::Handle 0 + IO::Select 0 + IPC::Open3 0 + POSIX 0 + Socket 0 + Test::More 0 + XSLoader 0 + lib 0 + perl 5.008001 + libnet-3.15 + pathname: S/SH/SHAY/libnet-3.15.tar.gz + provides: + Net undef + Net::Cmd 3.15 + Net::Config 3.15 + Net::Domain 3.15 + Net::FTP 3.15 + Net::FTP::A 3.15 + Net::FTP::E 3.15 + Net::FTP::I 3.15 + Net::FTP::L 3.15 + Net::FTP::dataconn 3.15 + Net::NNTP 3.15 + Net::NNTP::_SSL 3.15 + Net::Netrc 3.15 + Net::POP3 3.15 + Net::POP3::_SSL 3.15 + Net::SMTP 3.15 + Net::SMTP::_SSL 3.15 + Net::Time 3.15 + requirements: + Carp 0 + Errno 0 + Exporter 0 + ExtUtils::MakeMaker 6.64 + Fcntl 0 + File::Basename 0 + FileHandle 0 + Getopt::Std 0 + IO::File 0 + IO::Select 0 + IO::Socket 1.05 + POSIX 0 + Socket 2.016 + Symbol 0 + Time::Local 0 + constant 0 + perl 5.008001 + strict 0 + utf8 0 + vars 0 + warnings 0 + libwww-perl-6.78 + pathname: O/OA/OALDERS/libwww-perl-6.78.tar.gz + provides: + LWP 6.78 + LWP::Authen::Basic 6.78 + LWP::Authen::Digest 6.78 + LWP::Authen::Ntlm 6.78 + LWP::ConnCache 6.78 + LWP::Debug 6.78 + LWP::Debug::TraceHTTP 6.78 + LWP::DebugFile 6.78 + LWP::MemberMixin 6.78 + LWP::Protocol 6.78 + LWP::Protocol::cpan 6.78 + LWP::Protocol::data 6.78 + LWP::Protocol::file 6.78 + LWP::Protocol::ftp 6.78 + LWP::Protocol::gopher 6.78 + LWP::Protocol::http 6.78 + LWP::Protocol::loopback 6.78 + LWP::Protocol::mailto 6.78 + LWP::Protocol::nntp 6.78 + LWP::Protocol::nogo 6.78 + LWP::RobotUA 6.78 + LWP::Simple 6.78 + LWP::UserAgent 6.78 + requirements: + Digest::MD5 0 + Encode 2.12 + Encode::Locale 0 + ExtUtils::MakeMaker 0 + File::Copy 0 + File::Listing 6 + File::Temp 0 + Getopt::Long 0 + HTML::Entities 0 + HTML::HeadParser 3.71 + HTTP::Cookies 6 + HTTP::Date 6 + HTTP::Negotiate 6 + HTTP::Request 6.18 + HTTP::Request::Common 6.18 + HTTP::Response 6.18 + HTTP::Status 6.18 + IO::Select 0 + IO::Socket 0 + LWP::MediaTypes 6 + MIME::Base64 2.1 + Module::Load 0 + Net::FTP 2.58 + Net::HTTP 6.18 + Scalar::Util 0 + Try::Tiny 0 + URI 1.10 + URI::Escape 0 + WWW::RobotRules 6 + parent 0.217 + perl 5.008001 + strict 0 + warnings 0 + multidimensional-0.014 + pathname: I/IL/ILMARI/multidimensional-0.014.tar.gz + provides: + multidimensional 0.014 + requirements: + B::Hooks::OP::Check 0.19 + CPAN::Meta 2.112580 + ExtUtils::Depends 0 + ExtUtils::MakeMaker 0 + Test::More 0.88 + XSLoader 0 + if 0 + perl 5.008001 + strict 0 + warnings 0 + namespace-autoclean-0.31 + pathname: E/ET/ETHER/namespace-autoclean-0.31.tar.gz + provides: + namespace::autoclean 0.31 + requirements: + B 0 + B::Hooks::EndOfScope 0.12 + ExtUtils::MakeMaker 0 + List::Util 0 + namespace::clean 0.20 + perl 5.006 + strict 0 + warnings 0 + namespace-clean-0.27 + pathname: R/RI/RIBASUSHI/namespace-clean-0.27.tar.gz + provides: + namespace::clean 0.27 + requirements: + B::Hooks::EndOfScope 0.12 + ExtUtils::MakeMaker 0 + Package::Stash 0.23 + perl 5.008001 + podlators-v6.0.2 + pathname: R/RR/RRA/podlators-v6.0.2.tar.gz + provides: + Pod undef + Pod::Man v6.0.2 + Pod::ParseLink v6.0.2 + Pod::Text v6.0.2 + Pod::Text::Color v6.0.2 + Pod::Text::Overstrike v6.0.2 + Pod::Text::Termcap v6.0.2 + requirements: + ExtUtils::MakeMaker 0 + Pod::Simple 3.26 + perl 5.012 + strictures-2.000006 + pathname: H/HA/HAARG/strictures-2.000006.tar.gz + provides: + strictures 2.000006 + strictures::extra undef + requirements: + bareword::filehandles 0 + indirect 0 + multidimensional 0 + perl 5.006 + version-0.9933 + pathname: L/LE/LEONT/version-0.9933.tar.gz + provides: + version 0.9933 + version::regex 0.9933 + version::vpp 0.9933 + version::vxs 0.9933 + requirements: + ExtUtils::MakeMaker 0 + perl 5.006002 diff --git a/dist.ini b/dist.ini deleted file mode 100644 index f3d00c975..000000000 --- a/dist.ini +++ /dev/null @@ -1,61 +0,0 @@ -name = MetaCPAN -version = 0.0.1 -author = Moritz Onken -author = Olaf Alders -license = BSD -copyright_holder = Moritz Onken and Olaf Alders -copyright_year = 2011 - -; authordep Dist::Zilla::PluginBundle::JQUELIN -[@Filter] --bundle = @JQUELIN --remove = AutoVersion --remove = CheckChangelog - -[Prereqs] -Archive::Any = 0 -DateTime::Format::ISO8601 = 0 -Devel::ArgNames = 0 -ElasticSearch = 0.36 -EV = 0 -Gravatar::URL = 0 -Log::Log4perl::Appender::ScreenColoredLevels = 0 -MooseX::Attribute::Deflator = 2.1.5 -MooseX::ChainedAccessors = 0 -Mozilla::CA = 0 -Parse::CSV = 0 -Plack::Middleware::Header = 0 -Plack::Middleware::Session = 0 -Plack::Middleware::ServerStatus::Lite = 0 -Pod::Coverage::Moose = 0.02 -Starman = 0 -WWW::Mechanize::Cached = 0 -LWP::Protocol::https = 0 -Email::Sender::Simple = 0 -DBI = 1.616 -DBD::SQLite = 1.33 -IPC::Run3 = 0 -Parse::CPAN::Packages::Fast = 0.04 -Regexp::Common::time = 0 -PerlIO::gzip = 0 - -Catalyst = 5.90011 -Catalyst::Plugin::Unicode::Encoding = 0 -Catalyst::Controller::REST = 0.94 -Catalyst::Plugin::Authentication = 0 -Catalyst::Plugin::Session = 0 -Catalyst::Plugin::Session::State::Cookie = 0 -Catalyst::Plugin::Static::Simple = 0 -Catalyst::Action::RenderView = 0 -CHI = 0 -ElasticSearchX::Model = 0.1.0 -CatalystX::InjectComponent = 0 -Captcha::reCAPTCHA = 0.94 - -strictures = 1 -IO::All = 0 -JSON = 2 -YAML = 0 -Email::Address = 0 -File::Find = 0 -Path::Class = 0 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..99066703d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,64 @@ +services: + api-server: + build: + context: . + target: develop + volumes: + - './:/app/' + - '/app/local' + ports: + - '8001:8000' + environment: + # default is 120, shorten to work with compose label + COLUMNS: 96 + develop: + watch: + - path: ./cpanfile + action: rebuild + + api-test: + profiles: + - test + depends_on: + elasticsearch-test: + condition: service_healthy + build: + context: . + target: test + environment: + NET_ASYNC_HTTP_MAXCONNS: 1 + COLUMNS: 80 + ES: http://elasticsearch-test:9200 + HARNESS_ACTIVE: 1 + # Instantiate Catalyst models using metacpan_server_testing.conf + METACPAN_SERVER_CONFIG_LOCAL_SUFFIX: testing + MINICPAN: /CPAN + DEVEL_COVER_OPTIONS: +ignore,^t/|^test-data/|^etc/|^local/ + networks: + - elasticsearch + volumes: + - type: volume + source: elasticsearch-test + target: /usr/share/elasticsearch/data + + elasticsearch-test: + profiles: + - test + platform: linux/amd64 + image: elasticsearch:2.4 + environment: + - discovery.type=single-node + healthcheck: + timeout: 5s + start_period: 60s + test: ["CMD", "curl", "--fail", "http://localhost:9200/_cluster/health?wait_for_status=yellow&timeout=5s"] + ports: + - "9200" + networks: + - elasticsearch + +networks: + elasticsearch: + +volumes: + elasticsearch-test: diff --git a/docs/API-docs.md b/docs/API-docs.md new file mode 100644 index 000000000..10137aa6d --- /dev/null +++ b/docs/API-docs.md @@ -0,0 +1,365 @@ +# API Docs: v1 + +For an introduction to the MetaCPAN API (Application Program Interface) which requires no previous knowledge of MetaCPAN or ElasticSearch, see [the slides for "Abusing MetaCPAN for Fun and Profit"](https://www.slideshare.net/oalders/abusing-metacpan2013) or [watch the actual talk](https://www.youtube.com/watch?v=J8ymBuFlHQg). This API lets you programmatically access MetaCPAN from your own applications. + +There is also [a repository of examples](https://github.com/metacpan/metacpan-examples) you can play with to get up and running in a hurry. Rather than editing this wiki page, please send pull requests for the metacpan-examples repository. If you'd rather edit the wiki, please do, but sending the code pull requests is probably the most helpful way to approach this. + +_All of these URLs can be tested using the [MetaCPAN Explorer](https://explorer.metacpan.org)_ + +To learn more about the ElasticSearch query DSL (Domain-Specific Language) check out Clinton Gormley's [Terms of Endearment - ES Query DSL Explained](https://www.slideshare.net/clintongormley/terms-of-endearment-the-elasticsearch-query-dsl-explained) slides. + +The query syntax is explained on ElasticSearch's [reference page](https://www.elastic.co/guide/en/elasticsearch/reference/2.4/query-dsl.html). You can also check out this getting started tutorial about Elasticsearch [reference page](http://joelabrahamsson.com/elasticsearch-101/). + +## Being polite + +Currently, the only rules around using the API are to "be polite". We have enforced an upper limit of a size of 5,000 on search requests. If you need to fetch more than 5,000 items, you should look at using the scrolling API. Search this page for "scroll" to get an example using [Search::Elasticsearch](https://metacpan.org/pod/Search::Elasticsearch) or see the [Elasticsearch scroll docs](https://www.elastic.co/guide/en/elasticsearch/reference/2.4/search-request-scroll.html) if you are connecting in some other way. + +You can certainly scroll if you are fetching less than 5,000 items. You might want to do this if you are expecting a large data set, but will still need to run many requests to get all of the required data. + +Be aware that when you scroll, your docs will come back unsorted, as noted in the [ElasticSearch scan documentation](https://www.elastic.co/guide/en/elasticsearch/reference/2.4/search-request-search-type.html#scan). + +## Identifying Yourself + +Part of being polite is letting us know who you are and how to reach you. This is not mandatory, but please do consider adding your app to the [API-Consumers](https://github.com/metacpan/metacpan-api/wiki/fastapi-Consumers) page. + +## Available fields + +Available fields can be found by accessing the corresponding `_mapping` endpoint. + + +* [`/author/_mapping`](https://fastapi.metacpan.org/v1/author/_mapping) - [explore](https://explorer.metacpan.org/?url=/author/_mapping) +* [`/distribution/_mapping`](https://fastapi.metacpan.org/v1/distribution/_mapping) - [explore](https://explorer.metacpan.org/?url=/distribution/_mapping) +* [`/favorite/_mapping`](https://fastapi.metacpan.org/v1/favorite/_mapping) - [explore](https://explorer.metacpan.org/?url=/favorite/_mapping) +* [`/file/_mapping`](https://fastapi.metacpan.org/v1/file/_mapping) - [explore](https://explorer.metacpan.org/?url=/file/_mapping) +* [`/module/_mapping`](https://fastapi.metacpan.org/v1/module/_mapping) - [explore](https://explorer.metacpan.org/?url=/module/_mapping) +* [`/release/_mapping`](https://fastapi.metacpan.org/v1/release/_mapping) - [explore](https://explorer.metacpan.org/?url=/release/_mapping) + + +## Field documentation + +Fields are documented in the API codebase: https://github.com/metacpan/metacpan-api/tree/master/lib/MetaCPAN/Document Check the Pod for discussion of what the various fields represent. Be sure to have a look at https://github.com/metacpan/metacpan-api/blob/master/lib/MetaCPAN/Document/File.pm in particular as results for /module are really a thin wrapper around the `file` type. + +## Search without constraints + +Performing a search without any constraints is an easy way to get sample data + +* [`/author/_search`](https://fastapi.metacpan.org/v1/author/_search) +* [`/distribution/_search`](https://fastapi.metacpan.org/v1/distribution/_search) +* [`/favorite/_search`](https://fastapi.metacpan.org/v1/favorite/_search) +* [`/file/_search`](https://fastapi.metacpan.org/v1/file/_search) +* [`/release/_search`](https://fastapi.metacpan.org/v1/release/_search) + +## JSONP + +Simply add a `callback` query parameter with the name of your callback, and you'll get a JSONP response. + +* [/favorite?q=distribution:Moose&callback=cb](https://fastapi.metacpan.org/favorite?q=distribution:Moose&callback=cb) + +## GET convenience URLs + +You should be able to run most POST queries, but very few GET urls are currently exposed. However, these convenience endpoints can get you started. You should note that they behave differently than the POST queries in that they will return to you the latest version of a module or dist and they remove a lot of the verbose ElasticSearch data which wraps results. + +### `/distribution/{distribution}` + +The `/distribution` endpoint accepts the name of a `distribution` (e.g. [/distribution/Moose](https://fastapi.metacpan.org/v1/distribution/Moose)), which returns information about the distribution which is not specific to a version (like RT bug counts). + +### `/download_url/{module}` + +The `/download_url` endpoint exists specifically for the `cpanm` client. It takes a module name with an optional version (or range of versions) and an optional `dev` flag (for development releases) and returns a `download_url` as well as some other helpful info. + +Obviously anyone can use this endpoint, but we'll only consider changes to this endpoint after considering how `cpanm` might be affected. + +* [`https://fastapi.metacpan.org/v1/download_url/HTTP::Tiny`](https://fastapi.metacpan.org/v1/download_url/HTTP::Tiny) +* [`https://fastapi.metacpan.org/v1/download_url/Moose?version===0.01`](https://fastapi.metacpan.org/v1/download_url/Moose?version===0.01) +* [`https://fastapi.metacpan.org/v1/download_url/Moose?version=!=0.01`](https://fastapi.metacpan.org/v1/download_url/Moose?version=!=0.01) +* [`https://fastapi.metacpan.org/v1/download_url/Moose?version=<=0.02`](https://fastapi.metacpan.org/v1/download_url/Moose?version=<=0.02) +* [`https://fastapi.metacpan.org/v1/download_url/Try::Tiny?version=>0.21,<0.27,!=0.24`](https://fastapi.metacpan.org/v1/download_url/Try::Tiny?version=>0.21,<0.27,!=0.24) +* [`https://fastapi.metacpan.org/v1/download_url/Try::Tiny?version=>0.21,<0.27&dev=1`](https://fastapi.metacpan.org/v1/download_url/Try::Tiny?version=>0.21,<0.27&dev=1) +* [`https://fastapi.metacpan.org/v1/download_url/Try::Tiny?version=>0.21,<0.27,!=0.26&dev=1`](https://fastapi.metacpan.org/v1/download_url/Try::Tiny?version=>0.21,<0.27,!=0.26&dev=1) + +### `/release/{distribution}` + +### `/release/{author}/{release}` + +The `/release` endpoint accepts either the name of a `distribution` (e.g. [`/release/Moose`](https://fastapi.metacpan.org/v1/release/Moose)), which returns the most recent release of the distribution. Or provide the full path which consists of its `author` and the name of the `release` (e.g. [`/release/DOY/Moose-2.0001`](https://fastapi.metacpan.org/v1/release/DOY/Moose-2.0001)). + +### `/author/{author}` + +`author` refers to the pauseid of the author. It must be uppercased (e.g. [`/author/DOY`](https://fastapi.metacpan.org/v1/author/DOY)). + +### `/module/{module}` + +Returns the corresponding `file` of the latest version of the `module`. Considering that Moose-2.0001 is the latest release, the result of [`/module/Moose`](https://fastapi.metacpan.org/v1/module/Moose) is the same as [`/file/DOY/Moose-2.0001/lib/Moose.pm`](https://fastapi.metacpan.org/v1/file/DOY/Moose-2.0001/lib/Moose.pm). + +### `/pod/{module}` + +### `/pod/{author}/{release}/{path}` + +Returns the POD of the given module. You can change the output format by either passing a `content-type` query parameter (e.g. [`/pod/Moose?content-type=text/plain`](https://fastapi.metacpan.org/v1/pod/Moose?content-type=text/plain) or by adding an `Accept` header to the HTTP request. Valid content types are: + +* text/html (default) +* text/plain +* text/x-pod +* text/x-markdown + +### `/source/{module}` + +Returns the full source of the latest, authorized version of the given +`module`. + +## GET Searches + +Names of latest releases by OALDERS: + +[`https://fastapi.metacpan.org/v1/release/_search?q=author:OALDERS%20AND%20status:latest&fields=name,status&size=100`](https://fastapi.metacpan.org/v1/release/_search?q=author:OALDERS%20AND%20status:latest&fields=name,status&size=100) + +5,000 CPAN Authors: + +[`https://fastapi.metacpan.org/v1/author/_search?q=*&size=5000`](https://fastapi.metacpan.org/author/_search?q=*) + +All CPAN Authors Who Have Provided Twitter IDs: + +https://fastapi.metacpan.org/v1/author/_search?q=profile.name:twitter + +All CPAN Authors Who Have Updated MetaCPAN Profiles: + +https://fastapi.metacpan.org/v1/author/_search?q=updated:*&sort=updated:desc + +First 100 distributions which SZABGAB has given a ++: + +https://fastapi.metacpan.org/v1/favorite/_search?q=user:sWuxlxYeQBKoCQe1f-FQ_Q&size=100&fields=distribution + +The 100 most recent releases ( similar to https://metacpan.org/recent ) + +https://fastapi.metacpan.org/v1/release/_search?q=status:latest&fields=name,status,date&sort=date:desc&size=100 + +Number of ++'es that DOY's dists have received: + +https://fastapi.metacpan.org/v1/favorite/_search?q=author:DOY&size=0 + +List of users who have ++'ed DOY's dists and the dists they have ++'ed: + +https://fastapi.metacpan.org/v1/favorite/_search?q=author:DOY&fields=user,distribution + +Last 50 dists to get a ++: + +https://fastapi.metacpan.org/v1/favorite/_search?size=50&fields=author,user,release,date&sort=date:desc + +The Changes file of the Test-Simple distribution: + +https://fastapi.metacpan.org/v1/changes/Test-Simple + +## Querying the API with MetaCPAN::Client + +Perhaps the easiest way to get started using MetaCPAN is with [MetaCPAN::Client](https://metacpan.org/pod/MetaCPAN::Client). + +You can get started with [this example script to fetch author data](https://github.com/metacpan/metacpan-examples/blob/master/scripts/author/1-fetch-single-author.pl). + +## Querying the API with Search::Elasticsearch + +The API server at fastapi.metacpan.org is a wrapper around an [Elasticsearch](https://elasticsearch.org) instance. It adds support for the convenient GET URLs, handles authentication and does some access control. Therefore you can use the powerful API of [Search::Elasticsearch](https://metacpan.org/pod/Search::Elasticsearch) to query MetaCPAN. + +**NOTE**: The `cxn_pool => 'Static::NoPing'` is important because of the HTTP proxy we have in front of Elasticsearch. + +You can get started with [this example script to fetch author data](https://github.com/metacpan/metacpan-examples/blob/master/scripts/author/1-fetch-single-author-es.pl). + +## POST Searches + +Please feel free to add queries here as you use them in your own work, so that others can learn from you. + +### Downstream Dependencies + +This query returns a list of all releases which list MooseX::NonMoose as a +dependency. + +```sh +curl -XPOST https://fastapi.metacpan.org/v1/release/_search -d '{ + "size" : 5000, + "fields" : [ "distribution" ], + "query" : { + "bool" : { + "must" : [ + { "term" : { "dependency.module" : "MooseX::NonMoose" } }, + { "term" : { "maturity" : "released" } }, + { "term" : { "status" : "latest" } } + ] + } + } +}' +``` + +_Note it is also possible to use these queries in GET requests (useful for cross-domain JSONP requests) by appropriately encoding the JSON query into the `source` parameter of the URL. For example the query above [would become](https://fastapi.metacpan.org/v1/release/_search?source=%7B%0A%20%20%20%20%22size%22%20%3A%205000%2C%0A%20%20%20%20%22fields%22%20%3A%20%5B%20%22distribution%22%20%5D%2C%0A%20%20%20%20%22query%22%20%3A%20%7B%0A%20%20%20%20%20%20%20%20%22bool%22%20%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22must%22%20%3A%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%20%22term%22%20%3A%20%7B%20%22dependency.module%22%20%3A%20%22MooseX%3A%3ANonMoose%22%20%7D%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%20%22term%22%20%3A%20%7B%20%22maturity%22%20%3A%20%22released%22%20%7D%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%20%22term%22%20%3A%20%7B%20%22status%22%20%3A%20%22latest%22%20%7D%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%7D):_ + +``` +curl 'https://fastapi.metacpan.org/v1/release/_search?source=%7B%0A%20%20%20%20%22size%22%20%3A%205000%2C%0A%20%20%20%20%22fields%22%20%3A%20%5B%20%22distribution%22%20%5D%2C%0A%20%20%20%20%22query%22%20%3A%20%7B%0A%20%20%20%20%20%20%20%20%22bool%22%20%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22must%22%20%3A%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%20%22term%22%20%3A%20%7B%20%22dependency.module%22%20%3A%20%22MooseX%3A%3ANonMoose%22%20%7D%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%20%22term%22%20%3A%20%7B%20%22maturity%22%20%3A%20%22released%22%20%7D%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%20%22term%22%20%3A%20%7B%20%22status%22%20%3A%20%22latest%22%20%7D%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%7D' +``` + +### [The size of the CPAN unpacked](https://github.com/metacpan/metacpan-examples/blob/master/scripts/file/5-size-of-cpan.pl) + + +### Get license types of all releases in an arbitrary time span: + +```sh +curl -XPOST https://fastapi.metacpan.org/v1/release/_search?size=100 -d '{ + "query" : { + "range" : { + "date" : { + "gte" : "2010-06-05T00:00:00", + "lte" : "2011-06-05T00:00:00" + } + } + }, + "fields": [ "license", "name", "distribution", "date", "version_numified" ] +}' +``` + +### Aggregate by license: + +```sh +curl -XPOST https://fastapi.metacpan.org/v1/release/_search -d '{ + "query" : { + "match_all" : {} + }, + "aggs" : { + "license" : { + "terms" : { + "field" : "license" + } + } + }, + "size" : 0 +}' +``` + +### Most used file names in the root directory of releases: + +```sh +curl -XPOST https://fastapi.metacpan.org/v1/file/_search -d '{ + "query" : { + "term" : { "level" : 0 } + }, + "aggs" : { + "license" : { + "terms" : { + "size" : 100, + "field" : "name" + } + } + }, + "size" : 0 +}' +``` + +### Find all releases that contain a particular version of a module: + +```sh +curl -XPOST https://fastapi.metacpan.org/v1/file/_search -d '{ + "query" : { + "bool" : { + "must" : [ + { "term" : { "module.name" : "DBI::Profile" } }, + { "term" : { "module.version" : "2.014123" } } + ] + } + }, + "fields" : [ "release" ] +}' +``` + +### [Find all authors with Twitter in their profiles](https://github.com/metacpan/metacpan-examples/blob/master/scripts/author/1c-scroll-all-authors-with-twitter-es.pl) + +### [Get a leaderboard of ++'ed distributions](https://github.com/metacpan/metacpan-examples/blob/master/scripts/favorite/3-leaderboard-es.pl) + +### [Get a leaderboard of Authors with Most Uploads](https://github.com/metacpan/metacpan-examples/blob/master/scripts/release/2-author-upload-leaderboard-es.pl) + + +### [Search for a release by name](https://github.com/metacpan/metacpan-examples/blob/master/scripts/release/1-pkg2url-es.pl) + + +### Get the latest version numbers of your favorite modules + +Note that "size" should be the number of distributions you are looking for. + +```sh +lynx --dump --post_data https://fastapi.metacpan.org/v1/release/_search <set_indexed` **must** be called as early as possible, + otherwise things which inspect file.indexed or module.indexed will get + default values for those fields not _real_ values. + +* Similarly, `Document::File->set_authorized` should be called as soon after + `set_indexed` as possible. + + + +## Use cases to support + +Installing a package, various ways: + + cpanm Moose + cpanm Moose@2.1806 + cpanm Moose~'>2, <3' + cpanm --dev Moose + cpanm --dev Moose~'>2, <3' # maybe not? + +May need to inspect: + + release.authorized + release.status + file.indexed + file.authorized + file.status + module.indexed + module.authorized diff --git a/docs/logging.md b/docs/logging.md new file mode 100644 index 000000000..da725fbf5 --- /dev/null +++ b/docs/logging.md @@ -0,0 +1,16 @@ +# Logging + +Logging is done via Log::Contextual. There are three logger configs. These +can be found in the etc folder in this repository. + +## etc/metacpan.pl + +This is the default logger config + +## etc/metacpan_interactive.pl + +This logger config is used when scripts are run at the command line + +## etc/metacpan_testing.pl + +This logger config is used by the test suite. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 000000000..5c6c2ce42 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,25 @@ +# Testing + +## Releases + +When debugging the release indexing, try setting the bulk_size param to a low number, in order to make debugging easier. + + my $server = MetaCPAN::TestServer->new( ... ); + $server->index_releases( bulk_size => 1 ); + +You can enable Elasticsearch tracing when running tests at the command line: + + ES_TRACE=1 ./bin/prove t/darkpan.t + +You'll then find extensive logging information in `es.log`, at the top level of your Git checkout. + +## Indexing a Single Release + +If you want to speed up your debugging, you can index a solitary release using +the `MC_RELEASE` environment variable. + + MC_RELEASE=var/t/tmp/fakecpan/authors/id/L/LO/LOCAL/P-1.0.20.tar.gz ./bin/prove t/00_setup.t + +Or combine this with a test specific to the release. + + MC_RELEASE=var/t/tmp/fakecpan/authors/id/L/LO/LOCAL/P-1.0.20.tar.gz ./bin/prove t/00_setup.t t/release/p-1.0.20.t diff --git a/elasticsearch/cpanratings.pl b/elasticsearch/cpanratings.pl deleted file mode 100644 index 03683bc67..000000000 --- a/elasticsearch/cpanratings.pl +++ /dev/null @@ -1,276 +0,0 @@ -#!/usr/bin/perl -#=============================================================================== -# -# FILE: cpanratings.pl -# -# USAGE: ./cpanratings.pl -# -# DESCRIPTION: Screen-scrapper for cpanratings.perl.org's ratings and reviews -# -# OPTIONS: --- -# REQUIREMENTS: --- -# BUGS: --- -# NOTES: usage - perl cpanratings.pl Data::Dumper -# AUTHOR: J. Bobby Lopez (blopez), blopez@vmware.com, bobby.lopez@gmail.com -# COMPANY: CPAN-API Project -# VERSION: 1.0 -# CREATED: 11/11/10 05:01:10 PM -# REVISION: --- -#=============================================================================== - -#_______________________________________________________________[[ MODULES ]]_ - -#______________________________________[ Core or CPAN Modules ]_______________ - -use strict; -use warnings; -use Find::Lib '../lib'; -use Data::Dumper; -use Data::Dump; - -use List::Util qw(sum); -use WWW::Mechanize::Cached; -use HTML::TokeParser::Simple; -use JSON::XS; -use Parse::CSV; -use Path::Class::File; -use feature 'say'; - -#______________________________________[ Custom Modules ]_____________________ - -#use MetaCPAN; - -#__________________________________________________________________[[ SETUP ]]_ - -# Incoming arg = module name (e.g., Data::Dumper) -# would pull info from http://cpanratings.perl.org/dist/Data-Dumper - -my $dbg = 1; -my $cacher = WWW::Mechanize::Cached->new; -#my $es = MetaCPAN->new->es; - -prep_for_web(); - -#___________________________________________________________________[[ MAIN ]]_ - - - -my @to_insert = dump_all_ratings(); -#print Dumper( @to_insert ); - - -#dump_full_html(); # For testing - cleans up the HTML a bit before output -#print Dumper(\%ENV); - -#DONE - -#____________________________________________________________[[ SUBROUTINES ]]_ - -sub get_module_ratings -{ - my ($module) = @_; - $module =~ s/\:\:/-/g; - - my %json_hash; - my $base_url = "http://cpanratings.perl.org/dist/"; - my $url = $base_url . $module; - my $response = $cacher->get( $url ); - my $content = $response->content; - - if ( $content =~ "$module reviews" ) - { - %json_hash = populate_json_hash($content); - #my $json = dump_json(\%json_hash); - return %json_hash; - } - else - { - #print STDERR "404 Error with $module\n"; - return (); - } - -} - -sub dump_all_ratings -{ - my $csv_file = '/tmp/all_ratings.csv'; - my $file = Path::Class::File->new($csv_file); - my $fh = $file->openw(); - $cacher->get('http://cpanratings.perl.org/csv/all_ratings.csv'); - - print $fh $cacher->content; - - my $parser = Parse::CSV->new( - file => $csv_file, - fields => 'auto', - ); - - my @to_insert = (); - - my $limit = 99999; - my $i = 0; - while ( my $rating = $parser->fetch ) { - - my $dist_name = $rating->{distribution}; - chomp($dist_name); - if ( !defined( $dist_name ) ) { next; } - - $dbg && say "Trying |$dist_name| ...."; - my %fullratings = get_module_ratings($dist_name); - next if keys %fullratings != 2 - and ( $dbg && say "Skipping |$dist_name|..." ); - - $dbg && say "$dist_name: Avg Rating - " . $fullratings{avg_rating} ; - my $data = { - dist => $rating->{distribution}, - rating => $fullratings{avg_rating}, - reviews => $fullratings{reviews}, - }; - - my %es_insert = ( - index => { - index => 'cpan', - type => 'cpanratings', - id => $rating->{distribution}, - data => $data - } - ); - - push @to_insert, \%es_insert; - - last if $i >= $limit; - $i++; - } - - #my $result = $es->bulk( \@to_insert ); - - unlink $csv_file; - return @to_insert; -} - -sub populate_es -{ - my $csv_file = '/tmp/all_ratings.csv'; - my $file = Path::Class::File->new($csv_file); - my $fh = $file->openw(); - $cacher->get('http://cpanratings.perl.org/csv/all_ratings.csv'); - - print $fh $cacher->content; - - my $parser = Parse::CSV->new( - file => $csv_file, - fields => 'auto', - ); - - my @to_insert = (); - - while ( my $rating = $parser->fetch ) { - - my $dist_name = $rating->{distribution}; - - my $data = { - dist => $rating->{distribution}, - rating => $rating->{rating}, - review_count => $rating->{review_count}, - }; - - my %es_insert = ( - index => { - index => 'cpan', - type => 'cpanratings', - id => $rating->{distribution}, - data => $data - } - ); - - push @to_insert, \%es_insert; - - } - - #my $result = $es->bulk( \@to_insert ); - - unlink $csv_file; -} - -sub mean { - return sum(@_)/@_; -} - -#sub dump_full_html -#{ -# my $response = $cacher->get( $url ); -# my $content = $response->content; -# my $p = HTML::TokeParser::Simple->new(\$content); -# print "---- whole document ----\n"; -# while ( my $token = $p->get_token ) -# { -# print $token->as_is; -# } -# print "\n\n"; -#} - -sub dump_json -{ - my $hash_data = shift; - my $coder = JSON::XS->new->ascii->pretty->allow_nonref; - my $json = $coder->utf8->encode ($hash_data); - #binmode(STDOUT, ":utf8"); - return $json; -} - -sub prep_for_web -{ - if ( defined($ENV{'GATEWAY_INTERFACE'}) ) - { - print "Content-type: text/html\n\n"; - } -} - - -sub populate_json_hash -{ - my ($content) = @_; - my %json_hash; - my @avg_rating; - my $p = HTML::TokeParser::Simple->new(\$content); - my $i = 0; - while (my $token = $p->get_tag("h3")) - { - $token = $p->get_tag("a"); # start tag - $token = $p->get_token; # Module name inside - $token = $p->get_token; # end tag - $token = $p->get_token; # module version - my $module_version = $token->[1]; - $module_version =~ s/\n//g; - $module_version =~ s/.*\((.*)\).*/$1/; - - $token = $p->get_tag("img"); - my $rating = $token->[1]{'alt'} || "-"; - push @avg_rating, length($rating); - - $token = $p->get_tag("blockquote"); - my $review = $p->get_trimmed_text("/blockquote"); - - $token = $p->get_tag("a"); - my $reviewer = $p->get_trimmed_text("/a"); - my $date = $p->get_trimmed_text("br"); - chomp($date); - $date =~ s/(\d+-\d+-\d+)[[:space:]]+(\d+:\d+:\d+)/$1T$2/g; - $date =~ s/(?:^-|[[:space:]]+)//g; - - $json_hash{'reviews'}{$i}{'rating'} = length($rating); - $json_hash{'reviews'}{$i}{'review'} = $review; - $json_hash{'reviews'}{$i}{'reviewer'} = $reviewer; - $json_hash{'reviews'}{$i}{'review_date'} = $date; - $json_hash{'reviews'}{$i}{'module_version'} = $module_version; - - $i++; - } - - - if ( defined($json_hash{'reviews'}) ) - { - $json_hash{'avg_rating'} = sprintf( "%.2f", mean(@avg_rating) ); - } - return %json_hash; -} diff --git a/elasticsearch/cpants.pl b/elasticsearch/cpants.pl deleted file mode 100644 index 63d0d1137..000000000 --- a/elasticsearch/cpants.pl +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env perl - -use Data::Dump qw( dump ); -use JSON::Any; -use feature 'say'; -use WWW::Mechanize::Cached; - -my $j = JSON::Any->new; - -my $mech = WWW::Mechanize::Cached->new( autocheck => 0 ); - -$mech->get("http://www.cpantesters.org/distro/P/Plack-Middleware-HTMLify.json"); -my $reports = $j->decode( $mech->content ); - -my %results = ( ); -foreach my $test ( @{$reports} ) { - next if $test->{distversion} ne 'Plack-Middleware-HTMLify-0.1.1'; - ++$results{ $test->{state} }; -} - -say dump( \%results ); diff --git a/elasticsearch/index_perlmongers.pl b/elasticsearch/index_perlmongers.pl deleted file mode 100755 index 45bde5245..000000000 --- a/elasticsearch/index_perlmongers.pl +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env perl - -=head1 SYNOPSIS - -Loads PerlMonger groups into db. - - perl index_perlmongers.pl - -=cut - -use feature 'say'; -use Data::Dump qw( dump ); -use Find::Lib '../lib'; -use MetaCPAN::PerlMongers; - -my $author = MetaCPAN::PerlMongers->new; -my $result = $author->index_perlmongers; -say dump( $result ); - -$author->es->refresh_index( index => 'cpan' ); diff --git a/es/account/mapping.json b/es/account/mapping.json new file mode 100644 index 000000000..1d89039a6 --- /dev/null +++ b/es/account/mapping.json @@ -0,0 +1,33 @@ +{ + "dynamic": false, + "properties": { + "access_token": { + "dynamic": true, + "properties": { + "client": { + "type": "keyword" + }, + "token": { + "type": "keyword" + } + } + }, + "code": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "identity": { + "dynamic": false, + "properties": { + "key": { + "type": "keyword" + }, + "name": { + "type": "keyword" + } + } + } + } +} diff --git a/es/account/settings.json b/es/account/settings.json new file mode 100644 index 000000000..bbb95c38b --- /dev/null +++ b/es/account/settings.json @@ -0,0 +1,5 @@ +{ + "number_of_replicas": 1, + "number_of_shards": 1, + "refresh_interval": "1s" +} diff --git a/es/author/mapping.json b/es/author/mapping.json new file mode 100644 index 000000000..d643dfe50 --- /dev/null +++ b/es/author/mapping.json @@ -0,0 +1,115 @@ +{ + "dynamic": false, + "properties": { + "asciiname": { + "fields": { + "analyzed": { + "analyzer": "standard", + "fielddata": false, + "store": true, + "type": "text" + } + }, + "type": "keyword" + }, + "blog": { + "dynamic": true, + "properties": { + "feed": { + "type": "keyword" + }, + "url": { + "type": "keyword" + } + } + }, + "city": { + "type": "keyword" + }, + "country": { + "type": "keyword" + }, + "donation": { + "dynamic": true, + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + } + } + }, + "email": { + "type": "keyword" + }, + "gravatar_url": { + "type": "keyword" + }, + "is_pause_custodial_account": { + "type": "boolean" + }, + "location": { + "type": "geo_point" + }, + "name": { + "fields": { + "analyzed": { + "analyzer": "standard", + "fielddata": false, + "store": true, + "type": "text" + } + }, + "type": "keyword" + }, + "pauseid": { + "type": "keyword" + }, + "perlmongers": { + "dynamic": true, + "properties": { + "name": { + "type": "keyword" + }, + "url": { + "type": "keyword" + } + } + }, + "profile": { + "dynamic": false, + "include_in_root": true, + "properties": { + "id": { + "fields": { + "analyzed": { + "analyzer": "simple", + "fielddata": false, + "store": true, + "type": "text" + } + }, + "type": "keyword" + }, + "name": { + "type": "keyword" + } + }, + "type": "nested" + }, + "region": { + "type": "keyword" + }, + "updated": { + "format": "strict_date_optional_time||epoch_millis", + "type": "date" + }, + "user": { + "type": "keyword" + }, + "website": { + "type": "keyword" + } + } +} diff --git a/es/author/settings.json b/es/author/settings.json new file mode 100644 index 000000000..bbb95c38b --- /dev/null +++ b/es/author/settings.json @@ -0,0 +1,5 @@ +{ + "number_of_replicas": 1, + "number_of_shards": 1, + "refresh_interval": "1s" +} diff --git a/es/contributor/mapping.json b/es/contributor/mapping.json new file mode 100644 index 000000000..72017a284 --- /dev/null +++ b/es/contributor/mapping.json @@ -0,0 +1,23 @@ +{ + "dynamic": false, + "properties": { + "distribution": { + "type": "keyword" + }, + "email": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "pauseid": { + "type": "keyword" + }, + "release_author": { + "type": "keyword" + }, + "release_name": { + "type": "keyword" + } + } +} diff --git a/es/contributor/settings.json b/es/contributor/settings.json new file mode 100644 index 000000000..bbb95c38b --- /dev/null +++ b/es/contributor/settings.json @@ -0,0 +1,5 @@ +{ + "number_of_replicas": 1, + "number_of_shards": 1, + "refresh_interval": "1s" +} diff --git a/es/cover/mapping.json b/es/cover/mapping.json new file mode 100644 index 000000000..274adddc1 --- /dev/null +++ b/es/cover/mapping.json @@ -0,0 +1,34 @@ +{ + "dynamic": false, + "properties": { + "criteria": { + "dynamic": true, + "properties": { + "branch": { + "type": "float" + }, + "condition": { + "type": "float" + }, + "statement": { + "type": "float" + }, + "subroutine": { + "type": "float" + }, + "total": { + "type": "float" + } + } + }, + "distribution": { + "type": "keyword" + }, + "release": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } +} diff --git a/es/cover/settings.json b/es/cover/settings.json new file mode 100644 index 000000000..bbb95c38b --- /dev/null +++ b/es/cover/settings.json @@ -0,0 +1,5 @@ +{ + "number_of_replicas": 1, + "number_of_shards": 1, + "refresh_interval": "1s" +} diff --git a/es/cve/mapping.json b/es/cve/mapping.json new file mode 100644 index 000000000..8ec674c24 --- /dev/null +++ b/es/cve/mapping.json @@ -0,0 +1,36 @@ +{ + "dynamic": false, + "properties": { + "affected_versions": { + "type": "text" + }, + "cpansa_id": { + "type": "keyword" + }, + "cves": { + "type": "text" + }, + "description": { + "type": "text" + }, + "distribution": { + "type": "keyword" + }, + "references": { + "type": "text" + }, + "releases": { + "type": "keyword" + }, + "reported": { + "format": "strict_date_optional_time||epoch_millis", + "type": "date" + }, + "severity": { + "type": "text" + }, + "versions": { + "type": "keyword" + } + } +} diff --git a/es/cve/settings.json b/es/cve/settings.json new file mode 100644 index 000000000..bbb95c38b --- /dev/null +++ b/es/cve/settings.json @@ -0,0 +1,5 @@ +{ + "number_of_replicas": 1, + "number_of_shards": 1, + "refresh_interval": "1s" +} diff --git a/es/distribution/mapping.json b/es/distribution/mapping.json new file mode 100644 index 000000000..1e652f69b --- /dev/null +++ b/es/distribution/mapping.json @@ -0,0 +1,109 @@ +{ + "dynamic": false, + "properties": { + "bugs": { + "dynamic": true, + "properties": { + "github": { + "dynamic": true, + "properties": { + "active": { + "type": "integer" + }, + "closed": { + "type": "integer" + }, + "open": { + "type": "integer" + }, + "source": { + "type": "keyword" + } + } + }, + "rt": { + "dynamic": true, + "properties": { + "active": { + "type": "integer" + }, + "closed": { + "type": "integer" + }, + "new": { + "type": "integer" + }, + "open": { + "type": "integer" + }, + "patched": { + "type": "integer" + }, + "rejected": { + "type": "integer" + }, + "resolved": { + "type": "integer" + }, + "source": { + "type": "keyword" + }, + "stalled": { + "type": "integer" + } + } + } + } + }, + "external_package": { + "dynamic": true, + "properties": { + "cygwin": { + "type": "keyword" + }, + "debian": { + "type": "keyword" + }, + "fedora": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "repo": { + "dynamic": true, + "properties": { + "github": { + "dynamic": true, + "properties": { + "stars": { + "type": "integer" + }, + "watchers": { + "type": "integer" + } + } + } + } + }, + "river": { + "dynamic": true, + "properties": { + "bucket": { + "type": "integer" + }, + "bus_factor": { + "type": "integer" + }, + "immediate": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + } + } +} diff --git a/es/distribution/settings.json b/es/distribution/settings.json new file mode 100644 index 000000000..bbb95c38b --- /dev/null +++ b/es/distribution/settings.json @@ -0,0 +1,5 @@ +{ + "number_of_replicas": 1, + "number_of_shards": 1, + "refresh_interval": "1s" +} diff --git a/es/favorite/mapping.json b/es/favorite/mapping.json new file mode 100644 index 000000000..0aa0f6e88 --- /dev/null +++ b/es/favorite/mapping.json @@ -0,0 +1,24 @@ +{ + "dynamic": false, + "properties": { + "author": { + "type": "keyword" + }, + "date": { + "format": "strict_date_optional_time||epoch_millis", + "type": "date" + }, + "distribution": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "release": { + "type": "keyword" + }, + "user": { + "type": "keyword" + } + } +} diff --git a/es/favorite/settings.json b/es/favorite/settings.json new file mode 100644 index 000000000..bbb95c38b --- /dev/null +++ b/es/favorite/settings.json @@ -0,0 +1,5 @@ +{ + "number_of_replicas": 1, + "number_of_shards": 1, + "refresh_interval": "1s" +} diff --git a/es/file/mapping.json b/es/file/mapping.json new file mode 100644 index 000000000..12dc23970 --- /dev/null +++ b/es/file/mapping.json @@ -0,0 +1,242 @@ +{ + "dynamic": false, + "properties": { + "abstract": { + "fields": { + "analyzed": { + "analyzer": "standard", + "fielddata": false, + "store": true, + "type": "text" + } + }, + "type": "keyword" + }, + "author": { + "type": "keyword" + }, + "authorized": { + "type": "boolean" + }, + "binary": { + "type": "boolean" + }, + "date": { + "format": "strict_date_optional_time||epoch_millis", + "type": "date" + }, + "deprecated": { + "type": "boolean" + }, + "description": { + "type": "text" + }, + "dir": { + "type": "keyword" + }, + "directory": { + "type": "boolean" + }, + "dist_fav_count": { + "type": "integer" + }, + "distribution": { + "fields": { + "analyzed": { + "analyzer": "standard", + "fielddata": false, + "store": true, + "type": "text" + }, + "camelcase": { + "analyzer": "camelcase", + "store": true, + "type": "text" + }, + "lowercase": { + "analyzer": "lowercase", + "store": true, + "type": "text" + } + }, + "type": "keyword" + }, + "documentation": { + "fields": { + "analyzed": { + "analyzer": "standard", + "fielddata": false, + "store": true, + "type": "text" + }, + "camelcase": { + "analyzer": "camelcase", + "store": true, + "type": "text" + }, + "edge": { + "analyzer": "edge", + "store": true, + "type": "text" + }, + "edge_camelcase": { + "analyzer": "edge_camelcase", + "store": true, + "type": "text" + }, + "lowercase": { + "analyzer": "lowercase", + "store": true, + "type": "text" + } + }, + "type": "keyword" + }, + "documentation_length": { + "type": "integer" + }, + "download_url": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "indexed": { + "type": "boolean" + }, + "level": { + "type": "integer" + }, + "maturity": { + "type": "keyword" + }, + "mime": { + "type": "keyword" + }, + "module": { + "dynamic": false, + "include_in_root": true, + "properties": { + "associated_pod": { + "type": "text" + }, + "authorized": { + "type": "boolean" + }, + "indexed": { + "type": "boolean" + }, + "name": { + "fields": { + "analyzed": { + "analyzer": "standard", + "fielddata": false, + "store": true, + "type": "text" + }, + "camelcase": { + "analyzer": "camelcase", + "store": true, + "type": "text" + }, + "lowercase": { + "analyzer": "lowercase", + "store": true, + "type": "text" + } + }, + "type": "keyword" + }, + "version": { + "type": "keyword" + }, + "version_numified": { + "type": "float" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "path": { + "type": "keyword" + }, + "pod": { + "analyzer": "standard", + "fields": { + "analyzed": { + "analyzer": "standard", + "type": "text" + } + }, + "type": "text" + }, + "pod_lines": { + "type": "keyword" + }, + "release": { + "fields": { + "analyzed": { + "analyzer": "standard", + "fielddata": false, + "store": true, + "type": "text" + }, + "camelcase": { + "analyzer": "camelcase", + "store": true, + "type": "text" + }, + "lowercase": { + "analyzer": "lowercase", + "store": true, + "type": "text" + } + }, + "type": "keyword" + }, + "sloc": { + "type": "integer" + }, + "slop": { + "type": "integer" + }, + "stat": { + "dynamic": true, + "properties": { + "gid": { + "type": "long" + }, + "mode": { + "type": "integer" + }, + "mtime": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "uid": { + "type": "long" + } + } + }, + "status": { + "type": "keyword" + }, + "suggest": { + "analyzer": "simple", + "max_input_length": 50, + "preserve_position_increments": true, + "preserve_separators": true, + "type": "completion" + }, + "version": { + "type": "keyword" + }, + "version_numified": { + "type": "float" + } + } +} diff --git a/es/file/settings.json b/es/file/settings.json new file mode 100644 index 000000000..1cdf6e2f0 --- /dev/null +++ b/es/file/settings.json @@ -0,0 +1,53 @@ +{ + "analysis": { + "analyzer": { + "camelcase": { + "filter": [ + "lowercase", + "unique" + ], + "tokenizer": "camelcase", + "type": "custom" + }, + "edge": { + "filter": [ + "lowercase", + "edge" + ], + "tokenizer": "standard", + "type": "custom" + }, + "edge_camelcase": { + "filter": [ + "lowercase", + "edge" + ], + "tokenizer": "camelcase", + "type": "custom" + }, + "fulltext": { + "type": "english" + }, + "lowercase": { + "filter": "lowercase", + "tokenizer": "keyword" + } + }, + "filter": { + "edge": { + "max_gram": "20", + "min_gram": "1", + "type": "edge_ngram" + } + }, + "tokenizer": { + "camelcase": { + "pattern": "([^\\p{L}\\d]+)|(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)|(?<=[\\p{L}&&[^\\p{Lu}]])(?=\\p{Lu})|(?<=\\p{Lu})(?=\\p{Lu}[\\p{L}&&[^\\p{Lu}]])", + "type": "pattern" + } + } + }, + "number_of_replicas": 1, + "number_of_shards": 1, + "refresh_interval": "1s" +} diff --git a/es/mirror/mapping.json b/es/mirror/mapping.json new file mode 100644 index 000000000..eaebd2742 --- /dev/null +++ b/es/mirror/mapping.json @@ -0,0 +1,118 @@ +{ + "dynamic": false, + "properties": { + "A_or_CNAME": { + "type": "keyword" + }, + "aka_name": { + "type": "keyword" + }, + "ccode": { + "type": "keyword" + }, + "city": { + "fields": { + "analyzed": { + "analyzer": "standard", + "fielddata": false, + "store": true, + "type": "text" + } + }, + "type": "keyword" + }, + "contact": { + "dynamic": false, + "properties": { + "contact_site": { + "type": "keyword" + }, + "contact_user": { + "type": "keyword" + } + } + }, + "continent": { + "fields": { + "analyzed": { + "analyzer": "standard", + "fielddata": false, + "store": true, + "type": "text" + } + }, + "type": "keyword" + }, + "country": { + "fields": { + "analyzed": { + "analyzer": "standard", + "fielddata": false, + "store": true, + "type": "text" + } + }, + "type": "keyword" + }, + "dnsrr": { + "type": "keyword" + }, + "freq": { + "type": "keyword" + }, + "ftp": { + "type": "keyword" + }, + "http": { + "type": "keyword" + }, + "inceptdate": { + "format": "strict_date_optional_time||epoch_millis", + "type": "date" + }, + "location": { + "type": "geo_point" + }, + "name": { + "type": "keyword" + }, + "note": { + "type": "keyword" + }, + "org": { + "fields": { + "analyzed": { + "analyzer": "standard", + "fielddata": false, + "store": true, + "type": "text" + } + }, + "type": "keyword" + }, + "region": { + "fields": { + "analyzed": { + "analyzer": "standard", + "fielddata": false, + "store": true, + "type": "text" + } + }, + "type": "keyword" + }, + "reitredate": { + "format": "strict_date_optional_time||epoch_millis", + "type": "date" + }, + "rsync": { + "type": "keyword" + }, + "src": { + "type": "keyword" + }, + "tz": { + "type": "keyword" + } + } +} diff --git a/es/mirror/settings.json b/es/mirror/settings.json new file mode 100644 index 000000000..bbb95c38b --- /dev/null +++ b/es/mirror/settings.json @@ -0,0 +1,5 @@ +{ + "number_of_replicas": 1, + "number_of_shards": 1, + "refresh_interval": "1s" +} diff --git a/es/package/mapping.json b/es/package/mapping.json new file mode 100644 index 000000000..0248dfe82 --- /dev/null +++ b/es/package/mapping.json @@ -0,0 +1,23 @@ +{ + "dynamic": false, + "properties": { + "author": { + "type": "keyword" + }, + "dist_version": { + "type": "keyword" + }, + "distribution": { + "type": "keyword" + }, + "file": { + "type": "keyword" + }, + "module_name": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } +} diff --git a/es/package/settings.json b/es/package/settings.json new file mode 100644 index 000000000..bbb95c38b --- /dev/null +++ b/es/package/settings.json @@ -0,0 +1,5 @@ +{ + "number_of_replicas": 1, + "number_of_shards": 1, + "refresh_interval": "1s" +} diff --git a/es/permission/mapping.json b/es/permission/mapping.json new file mode 100644 index 000000000..31c2e36b6 --- /dev/null +++ b/es/permission/mapping.json @@ -0,0 +1,14 @@ +{ + "dynamic": false, + "properties": { + "co_maintainers": { + "type": "keyword" + }, + "module_name": { + "type": "keyword" + }, + "owner": { + "type": "keyword" + } + } +} diff --git a/es/permission/settings.json b/es/permission/settings.json new file mode 100644 index 000000000..bbb95c38b --- /dev/null +++ b/es/permission/settings.json @@ -0,0 +1,5 @@ +{ + "number_of_replicas": 1, + "number_of_shards": 1, + "refresh_interval": "1s" +} diff --git a/es/release/mapping.json b/es/release/mapping.json new file mode 100644 index 000000000..4ab1b1a28 --- /dev/null +++ b/es/release/mapping.json @@ -0,0 +1,211 @@ +{ + "dynamic": false, + "properties": { + "abstract": { + "fields": { + "analyzed": { + "analyzer": "standard", + "fielddata": false, + "store": true, + "type": "text" + } + }, + "type": "keyword" + }, + "archive": { + "type": "keyword" + }, + "author": { + "type": "keyword" + }, + "authorized": { + "type": "boolean" + }, + "changes_file": { + "type": "keyword" + }, + "checksum_md5": { + "type": "keyword" + }, + "checksum_sha256": { + "type": "keyword" + }, + "date": { + "format": "strict_date_optional_time||epoch_millis", + "type": "date" + }, + "dependency": { + "dynamic": false, + "include_in_root": true, + "properties": { + "module": { + "type": "keyword" + }, + "phase": { + "type": "keyword" + }, + "relationship": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + }, + "type": "nested" + }, + "deprecated": { + "type": "boolean" + }, + "distribution": { + "fields": { + "analyzed": { + "analyzer": "standard", + "fielddata": false, + "store": true, + "type": "text" + }, + "camelcase": { + "analyzer": "camelcase", + "store": true, + "type": "text" + }, + "lowercase": { + "analyzer": "lowercase", + "store": true, + "type": "text" + } + }, + "type": "keyword" + }, + "download_url": { + "type": "keyword" + }, + "first": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "license": { + "type": "keyword" + }, + "main_module": { + "type": "keyword" + }, + "maturity": { + "type": "keyword" + }, + "name": { + "fields": { + "analyzed": { + "analyzer": "standard", + "fielddata": false, + "store": true, + "type": "text" + }, + "camelcase": { + "analyzer": "camelcase", + "store": true, + "type": "text" + }, + "lowercase": { + "analyzer": "lowercase", + "store": true, + "type": "text" + } + }, + "type": "keyword" + }, + "provides": { + "type": "keyword" + }, + "resources": { + "dynamic": true, + "include_in_root": true, + "properties": { + "bugtracker": { + "dynamic": true, + "include_in_root": true, + "properties": { + "mailto": { + "type": "keyword" + }, + "web": { + "type": "keyword" + } + }, + "type": "nested" + }, + "homepage": { + "type": "keyword" + }, + "license": { + "type": "keyword" + }, + "repository": { + "dynamic": true, + "include_in_root": true, + "properties": { + "type": { + "type": "keyword" + }, + "url": { + "type": "keyword" + }, + "web": { + "type": "keyword" + } + }, + "type": "nested" + } + }, + "type": "nested" + }, + "stat": { + "dynamic": true, + "properties": { + "gid": { + "type": "long" + }, + "mode": { + "type": "integer" + }, + "mtime": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "uid": { + "type": "long" + } + } + }, + "status": { + "type": "keyword" + }, + "tests": { + "dynamic": true, + "properties": { + "fail": { + "type": "integer" + }, + "na": { + "type": "integer" + }, + "pass": { + "type": "integer" + }, + "unknown": { + "type": "integer" + } + } + }, + "version": { + "type": "keyword" + }, + "version_numified": { + "type": "float" + } + } +} diff --git a/es/release/settings.json b/es/release/settings.json new file mode 100644 index 000000000..1cdf6e2f0 --- /dev/null +++ b/es/release/settings.json @@ -0,0 +1,53 @@ +{ + "analysis": { + "analyzer": { + "camelcase": { + "filter": [ + "lowercase", + "unique" + ], + "tokenizer": "camelcase", + "type": "custom" + }, + "edge": { + "filter": [ + "lowercase", + "edge" + ], + "tokenizer": "standard", + "type": "custom" + }, + "edge_camelcase": { + "filter": [ + "lowercase", + "edge" + ], + "tokenizer": "camelcase", + "type": "custom" + }, + "fulltext": { + "type": "english" + }, + "lowercase": { + "filter": "lowercase", + "tokenizer": "keyword" + } + }, + "filter": { + "edge": { + "max_gram": "20", + "min_gram": "1", + "type": "edge_ngram" + } + }, + "tokenizer": { + "camelcase": { + "pattern": "([^\\p{L}\\d]+)|(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)|(?<=[\\p{L}&&[^\\p{Lu}]])(?=\\p{Lu})|(?<=\\p{Lu})(?=\\p{Lu}[\\p{L}&&[^\\p{Lu}]])", + "type": "pattern" + } + } + }, + "number_of_replicas": 1, + "number_of_shards": 1, + "refresh_interval": "1s" +} diff --git a/es/session/mapping.json b/es/session/mapping.json new file mode 100644 index 000000000..d4f130c22 --- /dev/null +++ b/es/session/mapping.json @@ -0,0 +1,3 @@ +{ + "dynamic": false +} diff --git a/es/session/settings.json b/es/session/settings.json new file mode 100644 index 000000000..bbb95c38b --- /dev/null +++ b/es/session/settings.json @@ -0,0 +1,5 @@ +{ + "number_of_replicas": 1, + "number_of_shards": 1, + "refresh_interval": "1s" +} diff --git a/es/settings.json b/es/settings.json new file mode 100644 index 000000000..1cdf6e2f0 --- /dev/null +++ b/es/settings.json @@ -0,0 +1,53 @@ +{ + "analysis": { + "analyzer": { + "camelcase": { + "filter": [ + "lowercase", + "unique" + ], + "tokenizer": "camelcase", + "type": "custom" + }, + "edge": { + "filter": [ + "lowercase", + "edge" + ], + "tokenizer": "standard", + "type": "custom" + }, + "edge_camelcase": { + "filter": [ + "lowercase", + "edge" + ], + "tokenizer": "camelcase", + "type": "custom" + }, + "fulltext": { + "type": "english" + }, + "lowercase": { + "filter": "lowercase", + "tokenizer": "keyword" + } + }, + "filter": { + "edge": { + "max_gram": "20", + "min_gram": "1", + "type": "edge_ngram" + } + }, + "tokenizer": { + "camelcase": { + "pattern": "([^\\p{L}\\d]+)|(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)|(?<=[\\p{L}&&[^\\p{Lu}]])(?=\\p{Lu})|(?<=\\p{Lu})(?=\\p{Lu}[\\p{L}&&[^\\p{Lu}]])", + "type": "pattern" + } + } + }, + "number_of_replicas": 1, + "number_of_shards": 1, + "refresh_interval": "1s" +} diff --git a/etc/cron.d/metacpan b/etc/cron.d/metacpan deleted file mode 100644 index 010c0cb2d..000000000 --- a/etc/cron.d/metacpan +++ /dev/null @@ -1,9 +0,0 @@ -MAILTO=onken@netcubed.de -SHELL=/bin/bash -PATH=/home/metacpan/perl5/perlbrew/bin:/home/metacpan/perl5/perlbrew/perls/perl-5.14.0/bin:/usr/local/bin:/usr/bin:/bin:/usr/X11R6/bin - -# m h dom mon dow command -0 * * * * metacpan $HOME/api.metacpan.org/bin/metacpan author -0 * * * * metacpan $HOME/api.metacpan.org/bin/metacpan mirrors -/5 * * * * metacpan $HOME/api.authorized/bin/metacpan authorized -55 * * * * metacpan rsync -avz rsync://cpan-rsync.perl.org/CPAN/authors/id/ $HOME/CPAN/authors/id/ > /dev/null diff --git a/etc/init.d/metacpan-elasticsearch b/etc/init.d/metacpan-elasticsearch deleted file mode 100644 index e69de29bb..000000000 diff --git a/etc/init.d/metacpan-server b/etc/init.d/metacpan-server deleted file mode 100644 index f3357c07c..000000000 --- a/etc/init.d/metacpan-server +++ /dev/null @@ -1,152 +0,0 @@ -#! /bin/sh -### BEGIN INIT INFO -# Provides: metacpan-server -# Required-Start: $remote_fs $syslog -# Required-Stop: $remote_fs $syslog -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: Run the metacpan http server - -### END INIT INFO - -# Author: Moritz Onken - -# PATH should only include /usr/* if it runs after the mountnfs.sh script -PATH=/sbin:/usr/sbin:/bin:/usr/bin -DESC="MetaCPAN http server" -NAME=metacpan-server -DAEMON=/usr/local/bin/metacpan -DAEMON_ARGS="server --cpan /var/cpan" -PIDFILE=/var/run/$NAME.pid -SCRIPTNAME=/etc/init.d/$NAME - -# Exit if the package is not installed -[ -x "$DAEMON" ] || exit 0 - -# Read configuration variable file if it is present -[ -r /etc/default/$NAME ] && . /etc/default/$NAME - -# Load the VERBOSE setting and other rcS variables -. /lib/init/vars.sh - -# Define LSB log_* functions. -# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. -. /lib/lsb/init-functions - -# -# Function that starts the daemon/service -# -do_start() -{ - # Return - # 0 if daemon has been started - # 1 if daemon was already running - # 2 if daemon could not be started - start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ - || return 1 - start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \ - $DAEMON_ARGS \ - || return 2 - # Add code here, if necessary, that waits for the process to be ready - # to handle requests from services started subsequently which depend - # on this one. As a last resort, sleep for some time. -} - -# -# Function that stops the daemon/service -# -do_stop() -{ - # Return - # 0 if daemon has been stopped - # 1 if daemon was already stopped - # 2 if daemon could not be stopped - # other if a failure occurred - start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME - RETVAL="$?" - [ "$RETVAL" = 2 ] && return 2 - # Wait for children to finish too if this is a daemon that forks - # and if the daemon is only ever run from this initscript. - # If the above conditions are not satisfied then add some other code - # that waits for the process to drop all resources that could be - # needed by services started subsequently. A last resort is to - # sleep for some time. - start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON - [ "$?" = 2 ] && return 2 - # Many daemons don't delete their pidfiles when they exit. - rm -f $PIDFILE - return "$RETVAL" -} - -# -# Function that sends a SIGHUP to the daemon/service -# -do_reload() { - # - # If the daemon can reload its configuration without - # restarting (for example, when it is sent a SIGHUP), - # then implement that here. - # - start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME - return 0 -} - -case "$1" in - start) - [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" - do_start - case "$?" in - 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; - 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; - esac - ;; - stop) - [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" - do_stop - case "$?" in - 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; - 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; - esac - ;; - status) - status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? - ;; - #reload|force-reload) - # - # If do_reload() is not implemented then leave this commented out - # and leave 'force-reload' as an alias for 'restart'. - # - #log_daemon_msg "Reloading $DESC" "$NAME" - #do_reload - #log_end_msg $? - #;; - restart|force-reload) - # - # If the "reload" option is implemented then remove the - # 'force-reload' alias - # - log_daemon_msg "Restarting $DESC" "$NAME" - do_stop - case "$?" in - 0|1) - do_start - case "$?" in - 0) log_end_msg 0 ;; - 1) log_end_msg 1 ;; # Old process is still running - *) log_end_msg 1 ;; # Failed to start - esac - ;; - *) - # Failed to stop - log_end_msg 1 - ;; - esac - ;; - *) - #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 - echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 - exit 3 - ;; -esac - -: \ No newline at end of file diff --git a/etc/init.d/metacpan-watcher b/etc/init.d/metacpan-watcher deleted file mode 100644 index e69de29bb..000000000 diff --git a/etc/metacpan.pl b/etc/metacpan.pl deleted file mode 100644 index 67512ff91..000000000 --- a/etc/metacpan.pl +++ /dev/null @@ -1,23 +0,0 @@ -# do not edit this file -# create etc/metacpan_local.pl instead -use FindBin; - -{ - # ElasticSearch instance, can be either a single server - # or an arrayref of servers - es => ':9200', - # the port of the api server - port => '5000', - # log level - level => 'info', - # appender for Log4perl - # default layout is "%d %p{1} %c: %m{chomp}%n" - # can be overridden using the layout key - # defining logger in metacpan_local.pl will - # override and not append to this configuration - logger => [{ - class => 'Log::Log4perl::Appender::File', - filename => $FindBin::RealBin . '/../var/log/metacpan.log', - syswrite => 1, - }] -} \ No newline at end of file diff --git a/etc/metacpan_interactive.pl b/etc/metacpan_interactive.pl deleted file mode 100644 index 520df00b4..000000000 --- a/etc/metacpan_interactive.pl +++ /dev/null @@ -1,12 +0,0 @@ -use FindBin; -{ - level => 'info', - logger => [{ - class => 'Log::Log4perl::Appender::ScreenColoredLevels', - stdout => 0, - }, { - class => 'Log::Log4perl::Appender::File', - filename => $FindBin::RealBin . '/../var/log/metacpan.log', - syswrite => 1, - }] -} \ No newline at end of file diff --git a/etc/metacpan_testing.pl b/etc/metacpan_testing.pl deleted file mode 100644 index 3778209e4..000000000 --- a/etc/metacpan_testing.pl +++ /dev/null @@ -1,10 +0,0 @@ -{ - es => ':9900', - port => '5900', - level => 'warn', - cpan => 't/var/tmp/fakecpan', - logger => [{ - class => 'Log::Log4perl::Appender::Screen', - name => 'testing' - }] -} diff --git a/etc/nginx b/etc/nginx deleted file mode 100644 index 6293a5083..000000000 --- a/etc/nginx +++ /dev/null @@ -1,31 +0,0 @@ -server { - listen 80; - listen 443; - ssl on; - server_name api.beta.metacpan.org api.metacpan.org; - access_log /home/metacpan/api.metacpan.org/var/log/api/access.log; - error_log /home/metacpan/api.metacpan.org/var/log/api/error.log error; - location /v0 { - proxy_pass http://localhost:5000/; - proxy_redirect off; - rewrite ^/v0/(.*)$ /$1 break; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Port $server_port; - proxy_set_header X-Forwarded-Host $host; - } - - # ultimately this will go away, but we will still need to - # route /_search to the latest api server - location / { - proxy_pass http://localhost:5000/; - proxy_redirect off; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Port $server_port; - proxy_set_header X-Forwarded-Host $host; - } - -} diff --git a/git/hooks/pre-commit b/git/hooks/pre-commit new file mode 100755 index 000000000..9256ae257 --- /dev/null +++ b/git/hooks/pre-commit @@ -0,0 +1,15 @@ +#!/bin/bash + +declare -i status +status=0 + +PRECIOUS=$(which precious) +if [[ -z $PRECIOUS ]]; then + PRECIOUS=./bin/precious +fi + +if ! "$PRECIOUS" lint -s; then + status+=1 +fi + +exit $status diff --git a/git/setup.sh b/git/setup.sh new file mode 100755 index 000000000..da4cc2e94 --- /dev/null +++ b/git/setup.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +chmod +x git/hooks/pre-commit +cd .git/hooks +ln -s ../../git/hooks/pre-commit diff --git a/lib/Archive/Any/Plugin/Tar.pm b/lib/Archive/Any/Plugin/Tar.pm deleted file mode 100644 index 0d7c8a127..000000000 --- a/lib/Archive/Any/Plugin/Tar.pm +++ /dev/null @@ -1,51 +0,0 @@ -package Archive::Any::Plugin::Tar; -use strict; -use base 'Archive::Any::Plugin'; - -use Archive::Tar; -use Cwd; - -=head1 NAME - -Archive::Any::Plugin::Tar - Archive::Any wrapper around Archive::Tar - -=head1 SYNOPSIS - -Do not use this module directly. Instead, use Archive::Any. - -=cut - -sub can_handle { - return( - 'application/x-tar', - 'application/x-gtar', - 'application/x-gzip', - 'application/x-bzip2', - ); -} - -sub files { - my( $self, $file ) = @_; - my $t = Archive::Tar->new( $file ); - return $t->list_files; -} - -sub extract { - my ( $self, $file ) = @_; - - my $t = Archive::Tar->new( $file ); - return $t->extract; -} - -sub type { - my $self = shift; - return 'tar'; -} - -=head1 SEE ALSO - -Archive::Any, Archive::Tar - -=cut - -1; \ No newline at end of file diff --git a/lib/Catalyst/Action/Deserialize/MetaCPANSanitizedJSON.pm b/lib/Catalyst/Action/Deserialize/MetaCPANSanitizedJSON.pm new file mode 100644 index 000000000..e46d7b8e2 --- /dev/null +++ b/lib/Catalyst/Action/Deserialize/MetaCPANSanitizedJSON.pm @@ -0,0 +1,80 @@ +package Catalyst::Action::Deserialize::MetaCPANSanitizedJSON; + +use Moose; +use Cpanel::JSON::XS (); +use MetaCPAN::Server::QuerySanitizer (); +use namespace::autoclean; +use Try::Tiny qw( catch try ); + +extends 'Catalyst::Action::Deserialize::JSON'; + +around execute => sub { + my ( $orig, $self, $controller, $c ) = @_; + my $result; + + try { + $result = $self->$orig( $controller, $c ); + + # if sucessfully serialized + if ( $result eq '1' ) { + + # if we got something + if ( my $data = $c->req->data ) { + + # clean it + $c->req->data( + MetaCPAN::Server::QuerySanitizer->new( query => $data, ) + ->query ); + } + } + + foreach my $attr (qw( query_parameters parameters )) { + + # there's probably a more appropriate place for this + # but it's the same concept and we can reuse the error handling + if ( my $params = $c->req->$attr ) { + + # ES also accepts the content in the querystring + if ( exists $params->{source} ) { + if ( my $source = delete $params->{source} ) { + + # NOTE: merge $controller->{json_options} if we ever use it + my $json = Cpanel::JSON::XS->new->utf8; + + # if it decodes + if ( try { $source = $json->decode($source); } ) { + + # clean it + $source = MetaCPAN::Server::QuerySanitizer->new( + query => $source, )->query; + + # update the $req + $params->{source} = $json->encode($source); + $c->req->$attr($params); + } + } + } + } + } + } + catch { + my $e = $_[0]; + if ( try { $e->isa('MetaCPAN::Server::QuerySanitizer::Error') } ) { + + # this will return a 400 (text) through Catalyst::Action::Deserialize + $result = $e->message; + + # this is our custom version (403) that returns json + $c->detach( "/not_allowed", [ $e->message ] ); + } + else { + $result = $e; + } + }; + + return $result; +}; + +__PACKAGE__->meta->make_immutable; + +1; diff --git a/lib/Catalyst/Action/Serialize/MetaCPANSanitizedJSON.pm b/lib/Catalyst/Action/Serialize/MetaCPANSanitizedJSON.pm new file mode 100644 index 000000000..b1bd98bc1 --- /dev/null +++ b/lib/Catalyst/Action/Serialize/MetaCPANSanitizedJSON.pm @@ -0,0 +1,8 @@ +package Catalyst::Action::Serialize::MetaCPANSanitizedJSON; + +use Moose; +extends 'Catalyst::Action::Serialize::JSON'; + +__PACKAGE__->meta->make_immutable; + +1; diff --git a/lib/Catalyst/Authentication/Store/Proxy.pm b/lib/Catalyst/Authentication/Store/Proxy.pm index ba4484e1f..d941a60df 100644 --- a/lib/Catalyst/Authentication/Store/Proxy.pm +++ b/lib/Catalyst/Authentication/Store/Proxy.pm @@ -2,18 +2,19 @@ package Catalyst::Authentication::Store::Proxy; # ABSTRACT: Delegates authentication logic to the user object use Moose; -use Catalyst::Utils; +use Catalyst::Utils (); +use MetaCPAN::Types::TypeTiny qw( ClassName HashRef Str ); has user_class => ( is => 'ro', required => 1, - isa => 'Str', + isa => Str, lazy => 1, builder => '_build_user_class' ); -has handles => ( is => 'ro', isa => 'HashRef' ); -has config => ( is => 'ro', isa => 'HashRef' ); -has app => ( is => 'ro', isa => 'ClassName' ); +has handles => ( is => 'ro', isa => HashRef ); +has config => ( is => 'ro', isa => HashRef ); +has app => ( is => 'ro', isa => ClassName ); has realm => ( is => 'ro' ); sub BUILDARGS { @@ -50,7 +51,7 @@ sub new_object { sub from_session { my ( $self, $c, $frozenuser ) = @_; - my $user = $self->new_object( $self->config, $c ); + my $user = $self->new_object( $self->config, $c ); my $delegate = $self->handles->{from_session}; return $user->$delegate( $c, $frozenuser ); } @@ -63,7 +64,7 @@ sub for_session { sub find_user { my ( $self, $authinfo, $c ) = @_; - my $user = $self->new_object( $self->config, $c ); + my $user = $self->new_object( $self->config, $c ); my $delegate = $self->handles->{find_user}; return $user->$delegate( $authinfo, $c ); diff --git a/lib/Catalyst/Plugin/Session/Store/ElasticSearch.pm b/lib/Catalyst/Plugin/Session/Store/ElasticSearch.pm index a128e1361..68159b5cb 100644 --- a/lib/Catalyst/Plugin/Session/Store/ElasticSearch.pm +++ b/lib/Catalyst/Plugin/Session/Store/ElasticSearch.pm @@ -4,36 +4,26 @@ package Catalyst::Plugin::Session::Store::ElasticSearch; use Moose; extends 'Catalyst::Plugin::Session::Store'; -use List::MoreUtils qw(); -use MooseX::Types::ElasticSearch qw(:all); +use MetaCPAN::Types::TypeTiny qw( ES ); + +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Server::Config (); +use MetaCPAN::Util qw( true false ); has _session_es => ( - required => 1, - is => 'rw', - coerce => 1, - isa => ES, - default => sub { shift->_session_plugin_config->{servers} || ':9200' } -); -has _session_es_index => ( - required => 1, - is => 'rw', - default => sub { shift->_session_plugin_config->{index} || 'user' } -); -has _session_es_type => ( - required => 1, - is => 'rw', - default => sub { shift->_session_plugin_config->{type} || 'session' } + is => 'ro', + lazy => 1, + coerce => 1, + isa => ES, + default => + sub { MetaCPAN::Server::Config::config()->{elasticsearch_servers} }, ); sub get_session_data { my ( $self, $key ) = @_; if ( my ($sid) = $key =~ /^\w+:(.*)/ ) { my $data = eval { - $self->_session_es->get( - index => $self->_session_es_index, - type => $self->_session_es_type, - id => $sid, - ); + $self->_session_es->get( es_doc_path('session'), id => $sid, ); } || return undef; if ( $key =~ /^expires:/ ) { return $data->{_source}->{_expires}; @@ -49,12 +39,10 @@ sub store_session_data { if ( my ($sid) = $key =~ /^session:(.*)/ ) { $session->{_expires} = $self->session_expires; $self->_session_es->index( - index => $self->_session_es_index, - type => $self->_session_es_type, + es_doc_path('session'), id => $sid, - parent => $session->{__user} || "", - data => $session, - refresh => 1, + body => $session, + refresh => true, ); } } @@ -64,10 +52,9 @@ sub delete_session_data { if ( my ($sid) = $key =~ /^session:(.*)/ ) { eval { $self->_session_es->delete( - index => $self->_session_es_index, - type => $self->_session_es_type, + es_doc_path('session'), id => $sid, - refresh => 1, + refresh => true, ); }; } @@ -86,13 +73,11 @@ sub delete_expired_sessions { } Session Session::Store::ElasticSearch ); - + # defaults MyApp->config( 'Plugin::Session' => { servers => ':9200', - index => 'user', - type => 'session', } ); =head1 DESCRIPTION @@ -116,16 +101,3 @@ The ElasticSearch index to use. Defaults to C. =head2 type The ElasticSearch type to use. Defaults to C. - -=head1 MAP TO A USER DOCUMENT - -Usually you will want to map a session to a user account. You will -probably have a user document in ElasticSearch that you want to map -to the session. ElasticSearch can do this very efficiently by establishing -a parent/child relationship. L will set -the C<__user> attribute on the session once a user has been authorized. -This attribute will be used as the C<_parent> of the session. Make sure -you define the C<_parent> type in the session type mapping. - - $ curl -XPUT localhost:9200/user/session/_mapping -d ' - {"session":{"dynamic":false,"_parent":{"type":"account"}}}' diff --git a/lib/ElasticSearchX/Model/Document/Role.pm b/lib/ElasticSearchX/Model/Document/Role.pm new file mode 100644 index 000000000..6d2b58be2 --- /dev/null +++ b/lib/ElasticSearchX/Model/Document/Role.pm @@ -0,0 +1,24 @@ +package ElasticSearchX::Model::Document::Role; +use strict; +use warnings; + +use MetaCPAN::Model::Hacks; + +no warnings 'redefine'; + +my $_put = \&_put; +*_put = sub { + my ($self) = @_; + my $es = $self->index->model->es; + + my %return = &$_put; + + if ( $es->api_version le '6_0' ) { + return %return; + } + + delete $return{type}; + return %return; +}; + +1; diff --git a/lib/ElasticSearchX/Model/Document/Set.pm b/lib/ElasticSearchX/Model/Document/Set.pm new file mode 100644 index 000000000..7adc669ca --- /dev/null +++ b/lib/ElasticSearchX/Model/Document/Set.pm @@ -0,0 +1,63 @@ +package ElasticSearchX::Model::Document::Set; +use strict; +use warnings; + +use MetaCPAN::Model::Hacks; + +no warnings 'redefine'; + +our %query_override; +my $_build_query = \&_build_query; +*_build_query = sub { + my $query = $_build_query->(@_); + %$query = ( %$query, %query_override ); + return $query; +}; + +our %qs_override; +my $_build_qs = \&_build_qs; +*_build_qs = sub { + my $qs = $_build_qs->(@_); + %$qs = ( %$qs, %qs_override ); + return $qs; +}; + +# ESXM normally tries to use search_type => scan, which is deprecated or +# removed in newer Elasticsearch versions. Sorting on _doc gives the same +# optimization. +my $delete = \&delete; +*delete = sub { + local %qs_override = ( search_type => 'query_then_fetch' ); + local %query_override = ( sort => '_doc' ); + return $delete->(@_); +}; + +my $get = \&get; +*get = sub { + my ( $self, $args, $qs ) = @_; + if ( $self->es->api_version eq '2_0' ) { + goto &$get; + } + my %qs = %{ $qs || {} }; + if ( my $fields = $self->fields ) { + $qs{_source} = $fields; + local $self->{fields}; + return $get->( $self, $args, \%qs ); + } + goto &$get; +}; + +# ESXM will try to inflate based on the index/type stored in the result. We +# are using aliases, and ESXM doesn't know about the actual index that the +# docs are stored in. Instead, allow it to use the configured index/type for +# this doc set. +my $inflate_result = \&inflate_result; +*inflate_result = sub { + my ( $self, $res ) = @_; + my $new_res = {%$res}; + delete $new_res->{_index}; + delete $new_res->{_type}; + $self->$inflate_result($new_res); +}; + +1; diff --git a/lib/MetaCPAN/API.pm b/lib/MetaCPAN/API.pm new file mode 100644 index 000000000..86b29ab84 --- /dev/null +++ b/lib/MetaCPAN/API.pm @@ -0,0 +1,172 @@ +package MetaCPAN::API; + +=head1 DESCRIPTION + +This is the API Minion server. + + # Display information on jobs in queue + ./bin/run bin/api.pl minion job -s + +=cut + +use Mojo::Base 'Mojolicious'; + +use File::Temp (); +use List::Util qw( any ); +use MetaCPAN::Script::Runner (); +use Try::Tiny qw( catch try ); +use MetaCPAN::Server::Config (); + +sub startup { + my $self = shift; + + unless ( $self->config->{config_override} ) { + $self->config( MetaCPAN::Server::Config::config() ); + } + + die 'need secret' unless $self->config->{secret}; + + $self->secrets( [ $self->config->{secret} ] ); + + $self->static->paths( [ $self->home->child('root') ] ); + + $self->plugin( Minion => $self->_build_db_params ); + + $self->minion->add_task( + index_release => $self->_gen_index_task_sub('release') ); + + $self->minion->add_task( + index_latest => $self->_gen_index_task_sub('latest') ); + + $self->minion->add_task( + index_favorite => $self->_gen_index_task_sub('favorite') ); + + $self->_set_up_routes; +} + +sub _gen_index_task_sub { + my ( $self, $type ) = @_; + + return sub { + my ( $job, @args ) = @_; + + my @warnings; + local $SIG{__WARN__} = sub { + push @warnings, $_[0]; + warn $_[0]; + }; + + # @args could be ( '--latest', '/path/to/release' ); + unshift @args, $type; + + # Runner expects to have been called via CLI + local @ARGV = @args; + try { + MetaCPAN::Script::Runner->run(@args); + $job->finish( @warnings ? { warnings => \@warnings } : () ); + } + catch { + warn $_; + $job->fail( { + message => $_, + @warnings ? ( warnings => \@warnings ) : (), + } ); + }; + } +} + +sub _set_up_routes { + my $self = shift; + + my $r = $self->routes; + + my $admin = $r->under( + '/admin' => sub { + my $c = shift; + my $username = $c->session('github_username'); + if ( $self->_is_admin($username) ) { + return 1; + } + + # Direct non-admins away from the app + elsif ($username) { + $c->redirect_to('https://metacpan.org'); + return 0; + } + + # This is possibly a logged out admin + $c->redirect_to('/auth/github/authenticate'); + return 0; + } + ); + + $self->_set_up_oauth_routes; + $self->plugin( 'Minion::Admin' => { route => $admin->any('/minion') } ); +} + +sub _is_admin { + my $self = shift; + my $username = $ENV{HARNESS_ACTIVE} ? $ENV{FORCE_ADMIN_AUTH} : shift; + return 0 unless $username; + + my @admins = ( + 'haarg', 'jberger', 'mickeyn', 'oalders', + 'ranguard', 'reyjrar', 'ssoriche', + $ENV{HARNESS_ACTIVE} ? 'tester' : (), + ); + + return any { $username eq $_ } @admins; +} + +sub _build_db_params { + my $self = shift; + + my $db_params; + if ( $ENV{HARNESS_ACTIVE} ) { + my $file = File::Temp->new( UNLINK => 1, SUFFIX => '.db' ); + return { SQLite => 'sqlite:' . $file }; + } + + die "Unable to determine dsn from configuration" + unless $self->config->{minion_dsn}; + + if ( $self->config->{minion_dsn} =~ /^postgresql:/ ) { + return { Pg => $self->config->{minion_dsn} }; + } + + if ( $self->config->{minion_dsn} =~ /^sqlite:/ ) { + return { SQLite => $self->config->{minion_dsn} }; + } + + die "Unsupported Database in dsn: " . $self->config->{minion_dsn}; +} + +sub _set_up_oauth_routes { + my $self = shift; + + my $oauth = $self->config->{oauth}; + + # We could do better DRY here, but it might be more complicated than it's + # worth + + $self->plugin( + 'Web::Auth', + module => 'Github', + key => $oauth->{github}->{key}, + secret => $oauth->{github}->{secret}, + user_info => 1, + on_finished => sub { + my ( $c, $access_token, $account_info ) = @_; + my $username = $account_info->{login}; + $c->session( is_logged_in => 1 ); + $c->session( github_username => $username ); + if ( $self->_is_admin($username) ) { + $c->redirect_to('/admin'); + return; + } + $c->redirect_to( $self->config->{front_end_url} ); + }, + ); +} + +1; diff --git a/lib/MetaCPAN/API/Controller/Admin.pm b/lib/MetaCPAN/API/Controller/Admin.pm new file mode 100644 index 000000000..a7095cf89 --- /dev/null +++ b/lib/MetaCPAN/API/Controller/Admin.pm @@ -0,0 +1,15 @@ +package MetaCPAN::API::Controller::Admin; + +use Mojo::Base 'Mojolicious::Controller'; + +sub identity_search_form { } + +sub search_identities { + my $self = shift; + my $data = $self->model->user->lookup( $self->param('name'), + $self->param('key') ); + $self->stash( user_data => $data ); + $self->render('admin/search_identities'); +} + +1; diff --git a/lib/MetaCPAN/API/Model/Role/ES.pm b/lib/MetaCPAN/API/Model/Role/ES.pm new file mode 100644 index 000000000..080b0e079 --- /dev/null +++ b/lib/MetaCPAN/API/Model/Role/ES.pm @@ -0,0 +1,16 @@ +package MetaCPAN::API::Model::Role::ES; + +use Moose::Role; + +use MetaCPAN::Types::TypeTiny qw( Object ); + +has es => ( + is => 'ro', + isa => Object, + handles => { _run_query => 'search', }, + required => 1, +); + +no Moose::Role; +1; + diff --git a/lib/MetaCPAN/API/Model/User.pm b/lib/MetaCPAN/API/Model/User.pm new file mode 100644 index 000000000..5414be5c0 --- /dev/null +++ b/lib/MetaCPAN/API/Model/User.pm @@ -0,0 +1,32 @@ +package MetaCPAN::API::Model::User; + +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Moose; + +with 'MetaCPAN::API::Model::Role::ES'; + +sub lookup { + my ( $self, $name, $key ) = @_; + + my $query = { + bool => { + must => [ + { term => { 'identity.name' => $name } }, + { term => { 'identity.key' => $key } }, + ] + } + }; + + my $res = $self->_run_query( + es_doc_path('account'), + body => { query => $query }, + search_type => 'dfs_query_then_fetch', + ); + + return $res->{hits}{hits}[0]{_source}; +} + +__PACKAGE__->meta->make_immutable; + +1; + diff --git a/lib/MetaCPAN/API/Plugin/Model.pm b/lib/MetaCPAN/API/Plugin/Model.pm new file mode 100644 index 000000000..f33dd4b8e --- /dev/null +++ b/lib/MetaCPAN/API/Plugin/Model.pm @@ -0,0 +1,49 @@ +package MetaCPAN::API::Plugin::Model; + +use Mojo::Base 'Mojolicious::Plugin'; + +use Carp (); + +# Models from the catalyst app +use MetaCPAN::Query::Search (); + +# New models +use MetaCPAN::API::Model::Cover (); +use MetaCPAN::API::Model::Download (); +use MetaCPAN::API::Model::User (); + +has app => sub { Carp::croak 'app is required' }, weak => 1; + +has download => sub { + my $self = shift; + return MetaCPAN::API::Model::Download->new( es => $self->app->es ); +}; + +has search => sub { + my $self = shift; + return MetaCPAN::Query::Search->new( es => $self->app->es, ); +}; + +has user => sub { + my $self = shift; + return MetaCPAN::API::Model::User->new( es => $self->app->es ); +}; + +has cover => sub { + my $self = shift; + return MetaCPAN::API::Model::Cover->new( es => $self->app->es ); +}; + +sub register { + my ( $plugin, $app, $conf ) = @_; + $plugin->app($app); + + # cached models + $app->helper( 'model.download' => sub { $plugin->download } ); + $app->helper( 'model.search' => sub { $plugin->search } ); + $app->helper( 'model.user' => sub { $plugin->user } ); + $app->helper( 'model.cover' => sub { $plugin->cover } ); +} + +1; + diff --git a/lib/MetaCPAN/Document/Author.pm b/lib/MetaCPAN/Document/Author.pm index 5d3f2d7dd..48296bb18 100644 --- a/lib/MetaCPAN/Document/Author.pm +++ b/lib/MetaCPAN/Document/Author.pm @@ -1,151 +1,113 @@ package MetaCPAN::Document::Author; -use Moose; -use ElasticSearchX::Model::Document; -use Gravatar::URL (); -use MetaCPAN::Util; - -use MetaCPAN::Types qw(:all); -use MooseX::Types::Structured qw(Dict Tuple Optional); -use MooseX::Types::Moose qw/Int Num Str ArrayRef HashRef Undef/; -use ElasticSearchX::Model::Document::Types qw(:all); -use MooseX::Types::Common::String qw(NonEmptySimpleStr); - -=head1 PROPERTIES - -=head2 email - -=head2 website - -=head2 city - -=head2 region - -=head2 country - -=head2 name - -=head2 name.analyzed - -Self explanatory. - -=head2 pauseid -PAUSE ID of the author. - -=head2 dir - -Directory of the author. -Example: C<< id/P/PE/PERLER >> - -=head2 gravatar_url - -URL to the gravatar user picture. This URL is generated using the first email address supplied to L. - -=head2 profile +use MetaCPAN::Moose; -Object or array of user profiles. Example: - - [ { name => "amazon", id => "B002MRC39U" }, - { name => "stackoverflow", id => "brian-d-foy" } ] - -=head2 blog - -Object or array of blogs. Example: - - { feed => "http://blogs.perl.org/users/brian_d_foy/atom.xml", - url => "http://blogs.perl.org/users/brian_d_foy/" } - -=head2 perlmongers - -Object or array of perlmonger groups. Example: - - { url => "http://frankfurt.pm", name => "Frankfurt.pm" } - -=head2 donation - -Object or array of places where to donate. Example: - - { name => "paypal", id => "brian.d.foy@gmail.com" } - -=head2 location - -Array of longitude and latitude. Example: - - [12.5436, 7.2358] - -=head2 extra - -=head2 extra.analyzed - -This field can contain anything. It is serialized using JSON -and stored in the index. You can do full-text searches on the -analyzed JSON string. +# load order important for next 2 modules +use ElasticSearchX::Model::Document::Types qw( Location ); +use ElasticSearchX::Model::Document; -=cut +# load order not important +use Gravatar::URL (); +use MetaCPAN::Types qw( ESBool Profile ); +use MetaCPAN::Types::TypeTiny qw( + ArrayRef + ArrayRefPromote + Blog + Dict + HashRef + NonEmptySimpleStr + PerlMongers + Str +); +use MetaCPAN::Util qw(true false); has name => ( is => 'ro', required => 1, index => 'analyzed', - isa => NonEmptySimpleStr + isa => NonEmptySimpleStr, ); + has asciiname => ( is => 'ro', required => 1, index => 'analyzed', - isa => NonEmptySimpleStr, - required => 0 + isa => Str, + default => q{}, ); + has [qw(website email)] => - ( is => 'ro', required => 1, isa => ArrayRef, coerce => 1 ); -has pauseid => ( is => 'ro', required => 1, id => 1 ); -has user => ( is => 'rw' ); -has dir => ( is => 'ro', required => 1, lazy_build => 1 ); -has gravatar_url => ( is => 'ro', lazy_build => 1, isa => NonEmptySimpleStr ); + ( is => 'ro', required => 1, isa => ArrayRefPromote, coerce => 1 ); + +has pauseid => ( + is => 'ro', + required => 1, + id => 1, +); + +has user => ( + is => 'ro', + writer => '_set_user', + clearer => '_clear_user', +); + +has gravatar_url => ( + is => 'ro', + isa => NonEmptySimpleStr, + lazy => 1, + builder => '_build_gravatar_url', +); + has profile => ( is => 'ro', isa => Profile, coerce => 1, type => 'nested', - required => 0, include_in_root => 1, ); + has blog => ( - is => 'ro', - isa => Blog, - coerce => 1, - required => 0, - dynamic => 1, + is => 'ro', + isa => Blog, + coerce => 1, + dynamic => 1, ); + has perlmongers => ( - is => 'ro', - isa => PerlMongers, - coerce => 1, - required => 0, - dynamic => 1 + is => 'ro', + isa => PerlMongers, + coerce => 1, + dynamic => 1, ); + has donation => ( - is => 'ro', - isa => ArrayRef [ Dict [ name => NonEmptySimpleStr, id => Str ] ], - required => 0, - dynamic => 1 + is => 'ro', + isa => ArrayRef [ Dict [ name => NonEmptySimpleStr, id => Str ] ], + dynamic => 1, ); -has [qw(city region country)] => - ( is => 'ro', required => 0, isa => NonEmptySimpleStr ); -has location => ( is => 'ro', isa => Location, coerce => 1, required => 0 ); + +has [qw(city region country)] => ( is => 'ro', isa => NonEmptySimpleStr ); + +has location => ( is => 'ro', isa => Location, coerce => 1 ); + has extra => ( is => 'ro', - isa => 'HashRef', + isa => HashRef, source_only => 1, dynamic => 1, - required => 0 ); -has updated => ( is => 'ro', isa => 'DateTime', required => 0 ); -sub _build_dir { - my $pauseid = ref $_[0] ? shift->pauseid : shift; - return MetaCPAN::Util::author_dir($pauseid); -} +has updated => ( + is => 'ro', + isa => Str, +); + +has is_pause_custodial_account => ( + is => 'ro', + isa => ESBool, + coerce => 1, + default => sub {false}, +); sub _build_gravatar_url { my $self = shift; @@ -154,11 +116,11 @@ sub _build_gravatar_url { # because we want to show the author's CPAN identity. # Using another e-mail than the CPAN one removes flexibility for # the author and ultimately could be a privacy leak. - # The author can manage this identity both on his gravatar account - # (by assigning an image to his author@cpan.org) + # The author can manage this identity both on their gravatar account + # (by assigning an image to their author@cpan.org) # and now by changing this URL from metacpa.org return Gravatar::URL::gravatar_url( - email => $self->{pauseid} . '@cpan.org', + email => $self->pauseid . '@cpan.org', size => 130, https => 1, @@ -174,7 +136,8 @@ sub validate { if ( $attr->is_required && !exists $data->{ $attr->name } ) { push( @result, - { field => $attr->name, + { + field => $attr->name, message => $attr->name . ' is required' } ); @@ -194,3 +157,80 @@ sub validate { } __PACKAGE__->meta->make_immutable; +1; + +=pod + +=head1 PROPERTIES + +=head2 email + +=head2 website + +=head2 city + +=head2 region + +=head2 country + +=head2 name + +=head2 name.analyzed + +Self explanatory. + +=head2 pauseid + +PAUSE ID of the author. + +=head2 dir + +Directory of the author. +Example: C<< id/P/PE/PERLER >> + +=head2 gravatar_url + +URL to the gravatar user picture. This URL is generated using PAUSEID@cpan.org. + +=head2 profile + +Object or array of user profiles. Example: + + [ { name => "amazon", id => "B002MRC39U" }, + { name => "stackoverflow", id => "brian-d-foy" } ] + +=head2 blog + +Object or array of blogs. Example: + + { feed => "http://blogs.perl.org/users/brian_d_foy/atom.xml", + url => "http://blogs.perl.org/users/brian_d_foy/" } + +=head2 perlmongers + +Object or array of perlmonger groups. Example: + + { url => "http://frankfurt.pm", name => "Frankfurt.pm" } + +=head2 donation + +Object or array of places where to donate. Example: + + { name => "paypal", id => "brian.d.foy@gmail.com" } + +=head2 location + +Array of longitude and latitude. Example: + + [12.5436, 7.2358] + +=head2 extra + +=head2 extra.analyzed + +This field can contain anything. It is serialized using JSON +and stored in the index. You can do full-text searches on the +analyzed JSON string. + +=cut + diff --git a/lib/MetaCPAN/Document/Author/Profile.pm b/lib/MetaCPAN/Document/Author/Profile.pm index ebc11e6b4..497828b07 100644 --- a/lib/MetaCPAN/Document/Author/Profile.pm +++ b/lib/MetaCPAN/Document/Author/Profile.pm @@ -1,9 +1,28 @@ package MetaCPAN::Document::Author::Profile; + +use strict; +use warnings; + use Moose; use ElasticSearchX::Model::Document; + +with 'ElasticSearchX::Model::Document::EmbeddedRole'; + +use MetaCPAN::Types::TypeTiny qw( Str ); use MetaCPAN::Util; -has name => ( is => 'ro', required => 1, isa => 'Str' ); -has id => ( is => 'ro', isa => 'Str', analyzer => ['simple'] ); +has name => ( + is => 'ro', + isa => Str, + required => 1, +); + +has id => ( + is => 'ro', + isa => Str, + analyzer => ['simple'], +); __PACKAGE__->meta->make_immutable; + +1; diff --git a/lib/MetaCPAN/Document/CVE.pm b/lib/MetaCPAN/Document/CVE.pm new file mode 100644 index 000000000..155b1b57a --- /dev/null +++ b/lib/MetaCPAN/Document/CVE.pm @@ -0,0 +1,63 @@ +package MetaCPAN::Document::CVE; + +use MetaCPAN::Moose; + +use ElasticSearchX::Model::Document; +use MetaCPAN::Types::TypeTiny qw( ArrayRef Str ); + +has distribution => ( + is => 'ro', + isa => Str, + required => 1, +); + +has cpansa_id => ( + is => 'ro', + isa => Str, + required => 1, +); + +has description => ( + is => 'ro', + isa => Str, + required => 1, +); + +has severity => ( + is => 'ro', + isa => Str, + required => 1, +); + +has reported => ( + is => 'ro', + isa => Str, + required => 1, +); + +has affected_versions => ( + is => 'ro', + isa => ArrayRef, + required => 1, +); + +has cves => ( + is => 'ro', + isa => ArrayRef, + required => 1, +); + +has references => ( + is => 'ro', + isa => ArrayRef, + required => 1, +); + +has versions => ( + is => 'ro', + isa => ArrayRef, + required => 1, +); + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Document/Contributor.pm b/lib/MetaCPAN/Document/Contributor.pm new file mode 100644 index 000000000..dec572834 --- /dev/null +++ b/lib/MetaCPAN/Document/Contributor.pm @@ -0,0 +1,42 @@ +package MetaCPAN::Document::Contributor; + +use MetaCPAN::Moose; + +use ElasticSearchX::Model::Document; +use MetaCPAN::Types::TypeTiny qw( ArrayRef Str ); + +has distribution => ( + is => 'ro', + isa => Str, + required => 1, +); + +has release_author => ( + is => 'ro', + isa => Str, + required => 1, +); + +has release_name => ( + is => 'ro', + isa => Str, + required => 1, +); + +has pauseid => ( + is => 'ro', + isa => Str, +); + +has name => ( + is => 'ro', + isa => Str, +); + +has email => ( + is => 'ro', + isa => ArrayRef [Str], +); + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Document/Cover.pm b/lib/MetaCPAN/Document/Cover.pm new file mode 100644 index 000000000..1ab9a5991 --- /dev/null +++ b/lib/MetaCPAN/Document/Cover.pm @@ -0,0 +1,33 @@ +package MetaCPAN::Document::Cover; + +use MetaCPAN::Moose; + +use ElasticSearchX::Model::Document; +use MetaCPAN::Types::TypeTiny qw( HashRef Str ); + +has distribution => ( + is => 'ro', + isa => Str, + required => 1, +); + +has release => ( + is => 'ro', + isa => Str, + required => 1, +); + +has version => ( + is => 'ro', + isa => Str, + required => 1, +); + +has criteria => ( + is => 'ro', + isa => HashRef, + required => 1, +); + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Document/Dependency.pm b/lib/MetaCPAN/Document/Dependency.pm index 10b9723ab..e54f85fd9 100644 --- a/lib/MetaCPAN/Document/Dependency.pm +++ b/lib/MetaCPAN/Document/Dependency.pm @@ -1,14 +1,16 @@ package MetaCPAN::Document::Dependency; + +use strict; +use warnings; + use Moose; use ElasticSearchX::Model::Document; + +with 'ElasticSearchX::Model::Document::EmbeddedRole'; + use MetaCPAN::Util; has [qw(phase relationship module version)] => ( is => 'ro', required => 1 ); -has version_numified => - ( is => 'ro', required => 1, isa => 'Num', lazy_build => 1 ); - -sub _build_version_numified { - return MetaCPAN::Util::numify_version( shift->version ); -} __PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Document/Distribution.pm b/lib/MetaCPAN/Document/Distribution.pm index a0088b704..64c178664 100644 --- a/lib/MetaCPAN/Document/Distribution.pm +++ b/lib/MetaCPAN/Document/Distribution.pm @@ -1,28 +1,64 @@ package MetaCPAN::Document::Distribution; +use strict; +use warnings; +use namespace::autoclean; + use Moose; use ElasticSearchX::Model::Document; -use MetaCPAN::Types qw(BugSummary); -use MooseX::Types::Moose qw(ArrayRef); -use namespace::autoclean; -has name => ( is => 'ro', required => 1, id => 1 ); +use MetaCPAN::Types::TypeTiny qw( BugSummary RiverSummary ); +use MetaCPAN::Util qw(true false); + +has name => ( + is => 'ro', + required => 1, + id => 1, +); + has bugs => ( - is => 'rw', - isa => ArrayRef[BugSummary], - lazy => 1, - default => sub { [] }, + is => 'ro', + isa => BugSummary, dynamic => 1, + writer => '_set_bugs', ); -sub add_bugs { - my ( $self, $add ) = @_; - BugSummary->assert_valid($add); - my $bugs = { - ( map { $_->{source} => $_ } @{ $self->bugs } ), - $add->{source} => $add, - }; - $self->bugs( [ values %$bugs ] ); +has river => ( + is => 'ro', + isa => RiverSummary, + dynamic => 1, + writer => '_set_river', + default => sub { + +{ + bucket => 0, + bus_factor => 1, + immediate => 0, + total => 0, + }; + }, +); + +sub releases { + my $self = shift; + return $self->index->model->doc("release") + ->query( { term => { "distribution" => $self->name } } ); +} + +sub set_first_release { + my $self = shift; + + my @releases = $self->releases->sort( ["date"] )->all; + + my $first = shift @releases; + $first->_set_first(true); + $first->put; + + for my $rel (@releases) { + $rel->_set_first(false); + $rel->put; + } + + return $first; } __PACKAGE__->meta->make_immutable; diff --git a/lib/MetaCPAN/Document/Favorite.pm b/lib/MetaCPAN/Document/Favorite.pm index 3bd92532b..eaf7cbcf1 100644 --- a/lib/MetaCPAN/Document/Favorite.pm +++ b/lib/MetaCPAN/Document/Favorite.pm @@ -1,22 +1,36 @@ package MetaCPAN::Document::Favorite; + +use strict; +use warnings; + use Moose; use ElasticSearchX::Model::Document; -use MetaCPAN::Types qw(:all); -use DateTime; + +use DateTime (); use MetaCPAN::Util; -has id => ( is => 'ro', id => [qw(user distribution)] ); -has [qw(author release user distribution)] => ( is => 'ro', required => 1 ); +has id => ( + is => 'ro', + id => [qw(user distribution)], +); + +has [qw(author release user distribution)] => ( + is => 'ro', + required => 1, +); + +=head2 date + +L when the item was created. + +=cut + has date => ( is => 'ro', required => 1, isa => 'DateTime', - default => sub { DateTime->now } + default => sub { DateTime->now }, ); -sub _build_release_id { - my $self = shift; - return MetaCPAN::Util::digest( $self->author, $self->release ); -} - __PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Document/File.pm b/lib/MetaCPAN/Document/File.pm index 073c2617a..c9fd79c8a 100644 --- a/lib/MetaCPAN/Document/File.pm +++ b/lib/MetaCPAN/Document/File.pm @@ -1,50 +1,224 @@ package MetaCPAN::Document::File; + +use strict; +use warnings; +use utf8; + use Moose; use ElasticSearchX::Model::Document; -use URI::Escape (); -use MetaCPAN::Pod::XHTML; -use Pod::Text; -use Plack::MIME; -use List::MoreUtils qw(uniq); -use MetaCPAN::Util; -use MetaCPAN::Types qw(:all); -use MooseX::Types::Moose qw(ArrayRef); -use Encode; -use utf8; +use List::Util qw( any ); +use MetaCPAN::Document::Module (); +use MetaCPAN::Types qw( ESBool Module ); +use MetaCPAN::Types::TypeTiny qw( ArrayRef Int Maybe Num ScalarRef Stat Str ); +use MetaCPAN::Util qw(numify_version true false); +use Plack::MIME (); +use Pod::Text (); +use Try::Tiny qw( catch try ); + +Plack::MIME->add_type( '.t' => 'text/x-script.perl' ); +Plack::MIME->add_type( '.pod' => 'text/x-pod' ); +Plack::MIME->add_type( '.xs' => 'text/x-c' ); -Plack::MIME->add_type( ".t" => "text/x-script.perl" ); -Plack::MIME->add_type( ".pod" => "text/x-pod" ); -Plack::MIME->add_type( ".xs" => "text/x-c" ); +my @NOT_PERL_FILES = qw(SIGNATURE); + +sub BUILD { + my $self = shift; + + # force building of `mime` + $self->_build_mime; +} =head1 PROPERTIES +=head2 deprecated + +Indicates file deprecation (the abstract contains "DEPRECATED" or "DEPRECIATED", +or the x_deprecated flag is included in the corresponding "provides" entry in distribution metadata); +it is also set if the entire release is marked deprecated (see L). + +=cut + +has deprecated => ( + is => 'ro', + isa => ESBool, + default => sub {false}, + writer => '_set_deprecated', +); + =head2 abstract Abstract of the documentation (if any). This is built by parsing the C section. It also sets L if it succeeds. +=cut + +has section => ( + is => 'ro', + isa => Maybe [Str], + lazy => 1, + builder => '_build_section', + property => 0, +); + +my $RE_SECTION = qr/^\s*(\S+)((\h+-+\h+(.+))|(\r?\n\h*\r?\n\h*(.+)))?/ms; + +sub _build_section { + my $self = shift; + + my $text = ${ $self->content }; + my $section = MetaCPAN::Util::extract_section( $text, 'NAME' ); + + # if it's a POD file without a name section, let's try to generate + # an abstract and name based on filename + if ( !$section && $self->path =~ /\.pod$/ ) { + $section = $self->path; + $section =~ s{^(lib|pod|docs)/}{}; + $section =~ s{\.pod$}{}; + $section =~ s{/}{::}g; + } + + return undef unless ($section); + $section =~ s/^=\w+.*$//mg; + $section =~ s/X<.*?>//mg; + + return $section; +} + +has abstract => ( + required => 1, + is => 'ro', + isa => Maybe [Str], + lazy => 1, + builder => '_build_abstract', + index => 'analyzed', +); + +sub _build_abstract { + my $self = shift; + return undef unless ( $self->is_perl_file ); + + my $section = $self->section; + return undef unless $section; + + my $abstract; + + if ( $section =~ $RE_SECTION ) { + chomp( $abstract = $4 || $6 ) if ( $4 || $6 ); + } + if ($abstract) { + $abstract =~ s/^=\w+.*$//xms; + $abstract =~ s{\r?\n\h*\r?\n\h*.*$}{}xms; + $abstract =~ s{\n}{ }gxms; + $abstract =~ s{\s+$}{}gxms; + $abstract =~ s{(\s)+}{$1}gxms; + $abstract = MetaCPAN::Util::strip_pod($abstract); + } + return $abstract; +} + =head2 id -Unique identifier of the release. Consists of the L's pauseid and -the release L. See L. +Unique identifier of the release. +Consists of the L's pauseid, the release L, +and the file path. +See L. + +=cut + +has id => ( + is => 'ro', + id => [qw(author release path)], +); =head2 module An ArrayRef of L objects, that represent modules defined in that class (i.e. package declarations). +=cut + +has module => ( + is => 'ro', + isa => Module, + type => 'nested', + include_in_root => 1, + coerce => 1, + clearer => 'clear_module', + writer => '_set_module', + lazy => 1, + default => sub { [] }, +); + +=head2 download_url + +B + +Download URL of the release + +=cut + +has download_url => ( + is => 'ro', + required => 1 +); + =head2 date B -Release date (i.e. C of the tarball). +Release date (i.e. C of the archive file). + +=cut + +has date => ( + is => 'ro', + required => 1, + isa => 'DateTime', +); =head2 description Contains the C section of the POD if any. Will be stripped from whitespaces and POD commands. +=cut + +has description => ( + required => 1, + is => 'ro', + lazy => 1, + builder => '_build_description', + index => 'not_analyzed', +); + +sub _build_description { + my $self = shift; + return undef unless ( $self->is_perl_file ); + my $section + = MetaCPAN::Util::extract_section( ${ $self->content }, + 'DESCRIPTION' ); + return undef unless ($section); + + my $parser = Pod::Text->new; + my $text = q{}; + $parser->output_string( \$text ); + + try { + $parser->parse_string_document("=pod\n\n$section"); + } + catch { + warn $_; + }; + + return undef unless $text; + + $text =~ s/\s+/ /g; + $text =~ s/^\s+//; + $text =~ s/\s+$//; + return $text; +} + =head2 distribution =head2 distribution.analyzed @@ -65,26 +239,178 @@ release, unless there are only developer releases. Everything else is tagged C. Once a release is deleted from PAUSE it is tagged as C. +=cut + +has status => ( is => 'ro', required => 1, default => 'cpan' ); + +=head2 binary + +File is binary or not. + +=cut + +has binary => ( + is => 'ro', + isa => ESBool, + required => 1, + default => sub {false}, +); + +=head2 authorized + +See L. + +=cut + +has authorized => ( + required => 1, + is => 'ro', + isa => ESBool, + default => sub {true}, + writer => '_set_authorized', +); + =head2 maturity Maturity of the release. This can either be C or C. See L. +=cut + +has maturity => ( + is => 'ro', + required => 1, + default => 'released', +); + =head2 directory Return true if this object represents a directory. +=cut + +has directory => ( + is => 'ro', + required => 1, + isa => ESBool, + default => sub {false}, +); + =head2 documentation -Holds the name for the documentation in this file. +Holds the name for the documentation in this file. -If the file L section. If the file L and the -name from the C section matches on of the modules in L, +If the file L, the name is derived from the +C section. If the file L and the +name from the C section matches one of the modules in L, it returns the name. Otherwise it returns the name of the first module in L. If there are no modules in the file the documentation is set to C. +=cut + +has documentation => ( + is => 'ro', + isa => Maybe [Str], + lazy => 1, + index => 'analyzed', + builder => '_build_documentation', + predicate => 'has_documentation', + analyzer => [qw(standard camelcase lowercase edge edge_camelcase)], + clearer => 'clear_documentation', + writer => '_set_documentation', +); + +sub _build_documentation { + my $self = shift; + return undef unless ( $self->is_perl_file ); + + my $section = $self->section; + return undef unless $section; + + my $documentation; + + if ( $section =~ $RE_SECTION ) { + my $name = MetaCPAN::Util::strip_pod($1); + $documentation = $name if ( $name =~ /^[\w\.:\-_']+$/ ); + } + + $documentation = MetaCPAN::Util::strip_pod($documentation) + if $documentation; + + return undef unless length $documentation; + + # Modules to be indexed + my @indexed = grep { $_->indexed } @{ $self->module || [] }; + + # This is a Pod file, return its name + if ( $documentation && $self->is_pod_file ) { + return $documentation; + } + + # OR: found an indexed module with the same name + if ( $documentation && grep { $_->name eq $documentation } @indexed ) { + return $documentation; + } + + # OR: found an indexed module with a name + if ( my ($mod) = grep { defined $_->name } @indexed ) { + return $mod->name; + } + + # OR: we have a parsed documentation + return $documentation if defined $documentation; + + # OR: found ANY module with a name (better than nothing) + if ( my ($mod) = grep { defined $_->name } @{ $self->module || [] } ) { + return $mod->name; + } + + return undef; +} + +has documentation_length => ( + is => 'ro', + isa => Maybe [Int], + lazy => 1, + builder => '_build_documentation_length', +); + +sub _build_documentation_length { + my ($self) = @_; + return length( $self->documentation ); +} + +=head2 suggest + +Autocomplete info for documentation. + +=cut + +has suggest => ( + is => 'ro', + + # isa => Maybe [HashRef], # remarked: breaks the suggester + lazy => 1, + builder => '_build_suggest', +); + +sub _build_suggest { + my $self = shift; + my $doc = $self->documentation; + + # return +{} unless $doc; # remarked because of 'isa' + return unless $doc; + + my $weight = 1000 - length($doc); + $weight = 0 if $weight < 0; + + return +{ + input => [$doc], + weight => $weight, + }; +} + =head2 indexed B @@ -92,16 +418,118 @@ B Indicates whether the file should be included in the search index or not. See L for a more verbose explanation. +=cut + +has indexed => ( + required => 1, + is => 'ro', + isa => ESBool, + lazy => 1, + default => sub { + my ($self) = @_; + return false if $self->is_in_other_files; + return false if !$self->metadata->should_index_file( $self->path ); + return true; + }, + writer => '_set_indexed', +); + =head2 level Level of this file in the directory tree of the release (i.e. C has a level of C<0>). +=cut + +has level => ( + required => 1, + is => 'ro', + isa => Int, + lazy => 1, + builder => '_build_level', +); + +sub _build_level { + my $self = shift; + my @level = split( /\//, $self->path ); + return @level - 1; +} + =head2 pod Pure text format of the pod (see L). Consecutive whitespaces are removed to save space and for better snippet previews. +=cut + +has pod => ( + required => 1, + is => 'ro', + isa => ScalarRef, + lazy => 1, + builder => '_build_pod', + index => 'analyzed', + not_analyzed => 0, + store => 'no', + term_vector => 'with_positions_offsets', +); + +sub _build_pod { + my $self = shift; + return \'' unless ( $self->is_perl_file ); + + my $parser = Pod::Text->new( sentence => 0, width => 78 ); + + # We don't need to index pod errors. + $parser->no_errata_section(1); + + my $content = ${ $self->content }; + + # The pod parser is very liberal and will "start" a pod document when it + # sees /^=[a-zA-Z]/ even though it might be binary like /^=F\0?\{/. + # So munge any lines that might match but are not usual pod directives + # that people would use (we don't need to index non-regular pod). + # Also see the test and comments in t/document/file.t for how + # bizarre constructs are handled. + + $content =~ s/ + # Pod::Simple::parse_string_document() "supports \r, \n ,\r\n"... + (?: + \A|\r|\r\n|\n) # beginning of line + \K # (keep those characters) + + ( + =[a-zA-Z][a-zA-Z0-9]* # looks like pod + (?! # but followed by something that isn't pod: + [a-zA-Z0-9] # more pod chars (the star won't be greedy enough) + | \s # whitespace ("=head1 NAME\n", "=item\n") + | \Z # end of line or end of doc + ) + ) + + # Prefix (to hide from Pod parser) instead of removing. + /\0$1/gx; + + my $text = ""; + $parser->output_string( \$text ); + + try { + $parser->parse_string_document($content); + } + catch { + warn $_[0]; + }; + + $text =~ s/\s+/ /g; + $text =~ s/ \z//; + + # Remove any markers we put in the text. + # Should we remove other non-regular bytes that may come from the source? + $text =~ s/\0//g; + + return \$text; +} + =head2 pod_lines ArrayRef of ArrayRefs of offset and length of pod blocks. Example: @@ -110,107 +538,185 @@ ArrayRef of ArrayRefs of offset and length of pod blocks. Example: # of 10 lines each [[1,10], [15,10]] +=cut + +has pod_lines => ( + is => 'ro', + isa => ArrayRef, + type => 'integer', + lazy => 1, + builder => '_build_pod_lines', + index => 'no', +); + +sub _build_pod_lines { + my $self = shift; + return [] unless ( $self->is_perl_file ); + my ( $lines, $slop ) = MetaCPAN::Util::pod_lines( ${ $self->content } ); + $self->_set_slop( $slop || 0 ); + return $lines; +} + =head2 sloc Source Lines of Code. Strips empty lines, pod and C section from L and returns the number of lines. +=cut + +has sloc => ( + required => 1, + is => 'ro', + isa => Int, + lazy => 1, + builder => '_build_sloc', +); + +# Metrics from Perl::Metrics2::Plugin::Core. +sub _build_sloc { + my $self = shift; + return 0 unless ( $self->is_perl_file ); + + my @content = split( "\n", ${ $self->content } ); + my $pods = 0; + + # Use pod_lines data to remove pod content from string. + map { + splice( @content, $_->[0], $_->[1], map {''} 1 .. $_->[1] ) + } @{ $self->pod_lines }; + + my $sloc = 0; + while (@content) { + my $line = shift @content; + last if ( $line =~ /^\s*__(DATA|END)__/s ); + $sloc++ if ( $line !~ /^\s*#/ && $line =~ /\S/ ); + } + return $sloc; +} + =head2 slop -Source Lines of Pod. Returns the number of pod lines using L. +Source Lines of Pod. Returns the number of pod lines using L. + +=cut + +has slop => ( + required => 1, + is => 'ro', + isa => Int, + lazy => 1, + builder => '_build_slop', + writer => '_set_slop', +); + +sub _build_slop { + my $self = shift; + return 0 unless ( $self->is_perl_file ); + $self->_build_pod_lines; + + # danger! infinite recursion if not set by `_build_pod_lines` + # we should probably find a better solution -- Mickey + return $self->slop; +} + +=head2 stat + +L info of the archive file. Contains C, +C and C. + +=cut + +has stat => ( + is => 'ro', + isa => Stat, + dynamic => 1, +); + +=head2 version + +Contains the raw version string. + +=cut + +has version => ( is => 'ro', ); + +=head2 version_numified + +B + +Numeric representation of L. Contains 0 if there is no version or the +version could not be parsed. + +=cut + +has version_numified => ( + required => 1, + is => 'ro', + isa => Num, + lazy => 1, + builder => '_build_version_numified', +); + +sub _build_version_numified { + my $self = shift; + return numify_version( $self->version ); +} -=head2 stat +=head2 mime -L info of the tarball. Contains C, C, C, C -and C. +MIME type of file. Derived using L (for speed). -=head2 version +=cut -Contains the raw version string. +has mime => ( + required => 1, + is => 'ro', + lazy => 1, + builder => '_build_mime', +); -=head2 version_numified +sub _build_mime { + my $self = shift; + if ( !$self->directory + && $self->name !~ /\./ + && grep { $self->name ne $_ } @NOT_PERL_FILES ) + { + my $content = ${ $self->content }; + return "text/x-script.perl" if ( $content =~ /^#!.*?perl/ ); + } + else { + return Plack::MIME->mime_type( $self->name ) || 'text/plain'; + } +} -B, B +has [qw(path author name)] => ( is => 'ro', required => 1 ); -Numified version of L. Contains 0 if there is no version or the -version could not be parsed. +sub _build_path { + my $self = shift; + return join( '/', $self->release->name, $self->name ); +} -=cut +has dir => ( + is => 'ro', + isa => Str, + lazy => 1, + builder => '_build_dir', + index => 'not_analyzed' +); -has id => ( is => 'ro', id => [qw(author release path)] ); +sub _build_dir { + my $self = shift; + $DB::single = 1; + my $dir = $self->path; + $dir =~ s{/[^/]+$}{}; + return $dir; +} -has [qw(path author name)] => ( is => 'ro', required => 1 ); has [qw(release distribution)] => ( is => 'ro', required => 1, analyzer => [qw(standard camelcase lowercase)], ); -has module => ( - required => 0, - is => 'rw', - isa => Module, - type => 'nested', - include_in_root => 1, - coerce => 1, - clearer => 'clear_module', - lazy => 1, - default => sub { [] }, -); -has documentation => ( - required => 1, - is => 'rw', - lazy_build => 1, - index => 'analyzed', - predicate => 'has_documentation', - analyzer => [qw(standard camelcase lowercase)], - clearer => 'clear_documentation', -); -has date => ( is => 'ro', required => 1, isa => 'DateTime' ); -has stat => ( is => 'ro', isa => Stat, required => 0, dynamic => 1 ); -has binary => ( is => 'ro', isa => 'Bool', required => 1, default => 0 ); -has sloc => ( is => 'ro', required => 1, isa => 'Int', lazy_build => 1 ); -has slop => - ( is => 'ro', required => 1, isa => 'Int', is => 'rw', lazy_build => 1 ); -has pod_lines => ( - is => 'ro', - required => 1, - isa => 'ArrayRef', - type => 'integer', - lazy_build => 1, - index => 'no' -); - -has pod => ( - is => 'ro', - required => 1, - isa => 'ScalarRef', - lazy_build => 1, - index => 'analyzed', - not_analyzed => 0, - store => 'no', - term_vector => 'with_positions_offsets' -); - -has mime => ( is => 'ro', required => 1, lazy_build => 1 ); -has abstract => - ( is => 'ro', required => 1, lazy_build => 1, index => 'analyzed' ); -has description => - ( is => 'ro', required => 1, lazy_build => 1, index => 'analyzed' ); -has status => ( is => 'ro', required => 1, default => 'cpan' ); -has authorized => ( required => 1, is => 'rw', isa => 'Bool', default => 1 ); -has maturity => ( is => 'ro', required => 1, default => 'released' ); -has directory => ( is => 'ro', required => 1, isa => 'Bool', default => 0 ); -has level => ( is => 'ro', required => 1, isa => 'Int', lazy_build => 1 ); -has indexed => ( required => 1, is => 'rw', isa => 'Bool', default => 1 ); -has version => ( is => 'ro', required => 0 ); -has version_numified => - ( is => 'ro', isa => 'Num', lazy_build => 1, required => 1 ); - -sub _build_version_numified { - my $self = shift; - return 0 unless ( $self->version ); - return MetaCPAN::Util::numify_version( $self->version ); -} =head1 ATTRIBUTES @@ -218,29 +724,16 @@ These attributes are not stored. =head2 content -The content of the file. It is built by calling L and -stripping the C section for performance reasons. - -=head2 content_cb - -Callback, that returns the content of the as ScalarRef. +A scalar reference to the content of the file. =cut has content => ( - is => 'ro', - isa => 'ScalarRef', - lazy_build => 1, - property => 0, - required => 0 -); -has content_cb => ( is => 'ro', + isa => ScalarRef, + lazy => 1, + default => sub { \"" }, property => 0, - required => 0, - default => sub { - sub { \'' } - } ); =head2 local_path @@ -254,6 +747,20 @@ has local_path => ( property => 0, ); +=head2 metadata + +Reference to the L object of the release. + +=cut + +has metadata => ( + is => 'ro', + isa => 'CPAN::Meta', + lazy => 1, + default => sub { die 'meta attribute missing' }, + property => 0, +); + =head1 METHODS =head2 is_perl_file @@ -263,14 +770,8 @@ or if the file has no extension, is not a binary file and its size is less than 131072 bytes. This is an arbitrary limit but it keeps the pod parser happy and the indexer fast. -=head2 is_pod_file - -Retruns true if the file extension is C. - =cut -my @NOT_PERL_FILES = qw(SIGNATURE); - sub is_perl_file { my $self = shift; return 0 if ( $self->directory ); @@ -278,175 +779,20 @@ sub is_perl_file { return 1 if ( $self->mime eq "text/x-script.perl" ); return 1 if ( $self->name !~ /\./ - && !grep { $self->name eq $_ } @NOT_PERL_FILES + && !( grep { $self->name eq $_ } @NOT_PERL_FILES ) && !$self->binary && $self->stat->{size} < 2**17 ); return 0; } -sub is_pod_file { - shift->name =~ /\.pod$/i; -} - -sub _build_documentation { - my $self = shift; - $self->_build_abstract; - my $documentation = $self->documentation if ( $self->has_documentation ); - return undef unless ( ${ $self->pod } ); - my @indexed = grep { $_->indexed } @{ $self->module || [] }; - if ( $documentation && $self->is_pod_file ) { - return $documentation; - } - elsif ( $documentation && grep { $_->name eq $documentation } @indexed ) { - return $documentation; - } - elsif (@indexed) { - return $indexed[0]->name; - } - elsif ( !@{ $self->module || [] } ) { - return $documentation; - } - else { - return undef; - } -} - -sub _build_level { - my $self = shift; - my @level = split( /\//, $self->path ); - return @level - 1; -} - -sub _build_content { - my $self = shift; - my @content = split( "\n", ${ $self->content_cb->() } || '' ); - my $content = ""; - my $in_data = 0; # skip DATA section - while (@content) { - my $line = shift @content; - if ( $line =~ /^\s*__END__\s*$/ ) { - $in_data = 0; - } - elsif ( $line =~ /^\s*__DATA__\s*$/ ) { - $in_data++; - } - elsif ( $in_data && $line =~ /^=head1/ ) { - $in_data = 0; - } - next if ($in_data); - $content .= $line . "\n"; - } - return \$content; -} - -sub _build_mime { - my $self = shift; - if ( !$self->directory - && $self->name !~ /\./ - && grep { $self->name ne $_ } @NOT_PERL_FILES ) - { - my $content = ${ $self->content }; - return "text/x-script.perl" if ( $content =~ /^#!.*?perl/ ); - } - else { - return Plack::MIME->mime_type( $self->name ) || 'text/plain'; - } -} - -sub _build_description { - my $self = shift; - return undef unless ( $self->is_perl_file ); - my $section = MetaCPAN::Util::extract_section( ${ $self->content }, - 'DESCRIPTION' ); - return undef unless ($section); - my $parser = Pod::Text->new; - my $text = ""; - $parser->output_string( \$text ); - $parser->parse_string_document("=pod\n\n$section"); - $text =~ s/\s+/ /g; - $text =~ s/^\s+//; - $text =~ s/\s+$//; - return $text; -} - -sub _build_abstract { - my $self = shift; - return undef unless ( $self->is_perl_file ); - my $text = ${ $self->content }; - my ( $documentation, $abstract ); - my $section = MetaCPAN::Util::extract_section( $text, 'NAME' ); - return undef unless ($section); - $section =~ s/^=\w+.*$//mg; - $section =~ s/X<.*?>//mg; - if ( $section =~ /^\s*(\S+)((\h+-+\h+(.+))|(\r?\n\h*\r?\n\h*(.+)))?/ms ) { - chomp( $abstract = $4 || $6 ) if ( $4 || $6 ); - my $name = MetaCPAN::Util::strip_pod($1); - $documentation = $name if ( $name =~ /^[\w\.:\-_']+$/ ); - } - if ($abstract) { - $abstract =~ s/^=\w+.*$//xms; - $abstract =~ s{\r?\n\h*\r?\n\h*.*$}{}xms; - $abstract =~ s{\n}{ }gxms; - $abstract =~ s{\s+$}{}gxms; - $abstract =~ s{(\s)+}{$1}gxms; - $abstract = MetaCPAN::Util::strip_pod($abstract); - } - - if ($documentation) { - $self->documentation( MetaCPAN::Util::strip_pod($documentation) ); - } - return $abstract; - -} - -sub _build_path { - my $self = shift; - return join( '/', $self->release->name, $self->name ); -} - -sub _build_pod_lines { - my $self = shift; - return [] unless ( $self->is_perl_file ); - my ( $lines, $slop ) = MetaCPAN::Util::pod_lines( ${ $self->content } ); - $self->slop( $slop || 0 ); - return $lines; -} - -sub _build_slop { - my $self = shift; - return 0 unless ( $self->is_perl_file ); - $self->_build_pod_lines; - return $self->slop; -} +=head2 is_pod_file -# Copied from Perl::Metrics2::Plugin::Core -sub _build_sloc { - my $self = shift; - return 0 unless ( $self->is_perl_file ); - my @content = split( "\n", ${ $self->content } ); - my $pods = 0; - map { - splice( @content, $_->[0], $_->[1], map {''} 1 .. $_->[1] ) - } @{ $self->pod_lines }; - my $sloc = 0; - while (@content) { - my $line = shift @content; - last if ( $line =~ /^\s*__END__/s ); - $sloc++ if ( $line !~ /^\s*#/ && $line =~ /\S/ ); - } - return $sloc; -} +Returns true if the file extension is C. -sub _build_pod { - my $self = shift; - return \'' unless ( $self->is_perl_file ); - my $parser = Pod::Text->new( sentence => 0, width => 78 ); +=cut - my $text = ""; - $parser->output_string( \$text ); - $parser->parse_string_document( ${ $self->content } ); - $text =~ s/\s+/ /g; - return \$text; +sub is_pod_file { + shift->name =~ /\.pod$/i; } =head2 add_module @@ -460,7 +806,53 @@ sub add_module { my ( $self, @modules ) = @_; $_ = MetaCPAN::Document::Module->new($_) for ( grep { ref $_ eq 'HASH' } @modules ); - $self->module( [ @{ $self->module }, @modules ] ); + $self->_set_module( [ @{ $self->module }, @modules ] ); +} + +=head2 is_in_other_files + +Returns true if the file is one from the list below. + +=cut + +sub is_in_other_files { + my $self = shift; + my @other = qw( + AUTHORS + Build.PL + Changelog + ChangeLog + CHANGELOG + Changes + CHANGES + CONTRIBUTING + CONTRIBUTING.md + CONTRIBUTING.pod + Copying + COPYRIGHT + cpanfile + CREDITS + dist.ini + FAQ + INSTALL + INSTALL.md + INSTALL.pod + LICENSE + Makefile.PL + MANIFEST + META.json + META.yml + NEWS + README + README.md + README.pod + THANKS + Todo + ToDo + TODO + ); + + return any { $self->path eq $_ } @other; } =head2 set_indexed @@ -469,6 +861,10 @@ Expects a C<$meta> parameter which is an instance of L. For each package (L) in the file and based on L it is decided, whether the module should have a true L attribute. +If there are any packages with leading underscores, the module gets a false +L attribute, because PAUSE doesn't allow this kind of name for packages +(https://github.com/andk/pause/blob/master/lib/PAUSE/pmfile.pm#L249). + If L returns true but the package declaration uses the I hack, the L property is set to false. @@ -485,23 +881,61 @@ does not include any modules, the L property is true. sub set_indexed { my ( $self, $meta ) = @_; + + # modules explicitly listed in 'provides' should be indexed + foreach my $mod ( @{ $self->module } ) { + if ( exists $meta->provides->{ $mod->name } + and $self->path eq $meta->provides->{ $mod->name }{file} ) + { + $mod->_set_indexed(true); + return; + } + } + + # files listed under 'other files' are not shown in a search + if ( $self->is_in_other_files() ) { + foreach my $mod ( @{ $self->module } ) { + $mod->_set_indexed(false); + } + $self->_set_indexed(false); + return; + } + + # files under no_index directories should not be indexed + foreach my $dir ( @{ $meta->no_index->{directory} } ) { + if ( $self->path eq $dir or $self->path =~ /^$dir\// ) { + $self->_set_indexed(false); + return; + } + } + foreach my $mod ( @{ $self->module } ) { - $mod->indexed( - $meta->should_index_package( $mod->name ) - ? $mod->hide_from_pause( ${ $self->content } ) - ? 0 - : 1 - : 0 - ) unless ( $mod->indexed ); + if ( $mod->name !~ /^[A-Za-z]/ + or !$meta->should_index_package( $mod->name ) ) + { + $mod->_set_indexed(false); + next; + } + + $mod->_set_indexed(true); } - $self->indexed( - # .pm file with no package declaration but pod should be indexed - !@{ $self->module } || + if ( my $doc_name = $self->documentation ) { + + # normalize the documentation name for comparison the same way module + # names are normalized + my $normalized_doc_name = $doc_name =~ s{'}{::}gr; + $self->_set_indexed( + ( + # .pm file with no package declaration but pod should be indexed + !@{ $self->module } || # don't index if the documentation doesn't match any of its modules - !!grep { $self->documentation eq $_->name } @{ $self->module } - ) if ( $self->documentation ); + grep { $normalized_doc_name eq $_->name } + @{ $self->module } + ) ? true : false + ); + } } =head2 set_authorized @@ -531,181 +965,40 @@ as unauthorized as well. sub set_authorized { my ( $self, $perms ) = @_; - # only authorized perl distributions make it into the CPAN - return () if ( $self->distribution eq 'perl' ); - foreach my $module ( @{ $self->module } ) { - $module->authorized(0) - if ( $perms->{ $module->name } && !grep { $_ eq $self->author } - @{ $perms->{ $module->name } } ); + if ( $self->distribution eq 'perl' ) { + my $allowed = grep $_ eq $self->author, @{ $perms->{perl} }; + foreach my $module ( @{ $self->module } ) { + $module->_set_authorized( $allowed ? true : false ); + } + $self->_set_authorized( $allowed ? true : false ); } - $self->authorized(0) - if ( $self->authorized - && $self->documentation - && $perms->{ $self->documentation } - && !grep { $_ eq $self->author } - @{ $perms->{ $self->documentation } } ); - return grep { !$_->authorized && $_->indexed } @{ $self->module }; -} - -__PACKAGE__->meta->make_immutable; - -package MetaCPAN::Document::File::Set; -use Moose; -extends 'ElasticSearchX::Model::Document::Set'; - -my @ROGUE_DISTRIBUTIONS - = qw(kurila perl_debug perl-5.005_02+apache1.3.3+modperl pod2texi perlbench spodcxx Bundle-Everything); - -sub find { - my ( $self, $module ) = @_; - my @candidates = $self->filter( - { and => [ - { or => [ - { term => { 'file.module.name' => $module } }, - { term => { 'file.documentation' => $module } }, - ] - }, - { term => { 'file.indexed' => \1, } }, - { term => { status => 'latest', } }, - { not => - { filter => { term => { 'file.authorized' => \0 } } } - }, - ] + else { + foreach my $module ( @{ $self->module } ) { + $module->_set_authorized(false) + if ( $perms->{ $module->name } + && !grep { $_ eq $self->author } + @{ $perms->{ $module->name } } ); } - )->sort( - [ { 'date' => { order => "desc" } }, - 'mime', - { 'stat.mtime' => { order => 'desc' } } - ] - )->size(100)->all; - - my ($file) = grep { - grep { $_->indexed && $_->authorized && $_->name eq $module } - @{ $_->module || [] } - } grep { !$_->documentation || $_->documentation eq $module } @candidates; - - # REINDEX: after a full reindex, the rest of the sub can be replaced with - # return $file ? $file : shift @candidates; - return shift @candidates unless ($file); - - ($module) = grep { $_->name eq $module } @{ $file->module }; - return $file if ( $module->associated_pod ); - - # if there is a .pod file in the same release, we use that instead - if (my ($pod) = grep { - $_->release eq $file->release - && $_->author eq $file->author - && $_->is_pod_file - } @candidates - ) - { - $module->associated_pod( - join( "/", map { $pod->$_ } qw(author release path) ) ); + $self->_set_authorized(false) + if ( $self->authorized + && $self->documentation + && $perms->{ $self->documentation } + && !grep { $_ eq $self->author } + @{ $perms->{ $self->documentation } } ); } - return $file; + return grep { !$_->authorized && $_->indexed } @{ $self->module }; } -sub find_pod { - my ( $self, $module ) = @_; - my @files = $self->filter( - { and => [ - { term => { 'file.documentation' => $module } }, - { term => { 'file.indexed' => \1, } }, - { term => { status => 'latest', } }, - { not => - { filter => { term => { 'file.authorized' => \0 } } } - }, - ] - } - )->sort( - [ { 'date' => { order => "desc" } }, - 'mime', - { 'stat.mtime' => { order => 'desc' } } - ] - )->all; - my ($file) = grep { $_->is_pod_file } @files; - ($file) = grep { - grep { $_->indexed && $_->authorized && $_->name eq $module } - @{ $_->module || [] } - } @files unless ($file); - return $file ? $file : shift @files; -} +=head2 full_path -# return files that contain modules that match the given dist -# NOTE: these still need to be filtered by authorized/indexed -# TODO: test that we are getting the correct version (latest) -sub find_provided_by { - my ( $self, $release ) = @_; - return $self->filter( - { and => [ - { term => { 'release' => $release->{name} } }, - { term => { 'author' => $release->{author} } }, - { term => { 'file.module.authorized' => 1 } }, - { term => { 'file.module.indexed' => 1 } }, - ] - } - )->size(999)->all; -} +Concatenate L, L and L. -# filter find_provided_by results for indexed/authorized modules -# and return a list of package names -sub find_module_names_provided_by { - my ( $self, $release ) = @_; - my $mods = $self->inflate(0)->find_provided_by($release); - return ( - map { $_->{name} } - grep { $_->{indexed} && $_->{authorized} } - map { @{ $_->{_source}->{module} } } @{ $mods->{hits}->{hits} } - ); -} +=cut -sub prefix { - my ( $self, $prefix ) = @_; - my @query = split( /\s+/, $prefix ); - my $should = [ - map { - { field => { 'documentation.analyzed' => "$_*" } }, - { field => { 'documentation.camelcase' => "$_*" } } - } grep {$_} @query - ]; - return $self->query( - { filtered => { - query => { - custom_score => { - query => { bool => { should => $should } }, - script => - "_score - doc['documentation'].stringValue.length()/100" - }, - }, - filter => { - and => [ - { not => { - filter => { - or => [ - map { - +{ term => { - 'file.distribution' => $_ - } - } - } @ROGUE_DISTRIBUTIONS - - ] - } - } - }, - { exists => { field => 'documentation' } }, - { term => { 'file.indexed' => \1 } }, - { term => { 'file.status' => 'latest' } }, - { not => { - filter => - { term => { 'file.authorized' => \0 } } - } - } - ] - } - } - } - ); +sub full_path { + my $self = shift; + return join( '/', $self->author, $self->release, $self->path ); } __PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Document/Mirror.pm b/lib/MetaCPAN/Document/Mirror.pm index e6e8e2928..7b3b04e94 100644 --- a/lib/MetaCPAN/Document/Mirror.pm +++ b/lib/MetaCPAN/Document/Mirror.pm @@ -1,18 +1,45 @@ package MetaCPAN::Document::Mirror; + +use strict; +use warnings; + use Moose; +use MooseX::Types::ElasticSearch qw( Location ); use ElasticSearchX::Model::Document; -use ElasticSearchX::Model::Document::Types qw(:all); -use MetaCPAN::Util; +use MetaCPAN::Types::TypeTiny qw( Dict Str ); + +has name => ( + is => 'ro', + required => 1, + id => 1, +); + +has [qw(org city region country continent)] => ( + is => 'ro', + index => 'analyzed', +); -has name => ( is => 'ro', required => 1, id => 1 ); -has [qw(org city region country continent)] => - ( is => 'ro', index => 'analyzed' ); has [qw(tz src http rsync ftp freq note dnsrr ccode aka_name A_or_CNAME)] => ( is => 'ro' ); -has location => ( is => 'ro', isa => Location, coerce => 1 ); -has contact => ( is => 'ro', required => 1, isa => 'ArrayRef' ); -has [qw(inceptdate reitredate)] => - ( is => 'ro', isa => 'DateTime', coerce => 1 ); + +has location => ( + is => 'ro', + isa => Location, + coerce => 1, +); + +has contact => ( + is => 'ro', + required => 1, + isa => Dict [ contact_site => Str, contact_user => Str ], +); + +has [qw(inceptdate reitredate)] => ( + is => 'ro', + isa => 'DateTime', + coerce => 1, +); __PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Document/Module.pm b/lib/MetaCPAN/Document/Module.pm index 75dd4c949..a3901bae1 100644 --- a/lib/MetaCPAN/Document/Module.pm +++ b/lib/MetaCPAN/Document/Module.pm @@ -1,14 +1,25 @@ package MetaCPAN::Document::Module; + +use strict; +use warnings; + use Moose; use ElasticSearchX::Model::Document; -use MetaCPAN::Util; + +with 'ElasticSearchX::Model::Document::EmbeddedRole'; + +use MetaCPAN::Types qw( ESBool ); +use MetaCPAN::Types::TypeTiny qw( Maybe Num Str ); +use MetaCPAN::Util qw(true false); =head1 SYNOPSIS -MetaCPAN::Document::Module->new( - { name => "Some::Module", - version => "1.1.1" - } ); + MetaCPAN::Document::Module->new( + { + name => "Some::Module", + version => "1.1.1" + } + ); =head1 PROPERTIES @@ -28,13 +39,6 @@ the C and the C property. Contains the raw version string. -=head2 version_numified - -B, B - -Numified version of L. Contains 0 if there is no version or the -version could not be parsed. - =head2 indexed B @@ -45,7 +49,7 @@ from the index. =head1 METHODS -=head2 hide_from_pause( $content ) +=head2 hide_from_pause( $content, $file_name ) Using this pragma, you can hide a module from the CPAN indexer: @@ -59,18 +63,44 @@ not declared in one line, the module is considered not-indexed. has name => ( is => 'ro', + isa => Str, required => 1, index => 'analyzed', analyzer => [qw(standard camelcase lowercase)], ); + has version => ( is => 'ro' ); -has version_numified => - ( is => 'ro', isa => 'Num', lazy_build => 1, required => 1 ); -has indexed => ( is => 'rw', required => 1, isa => 'Bool', default => 0 ); -has authorized => ( is => 'rw', required => 1, isa => 'Bool', default => 1 ); -# REINDEX: make 'ro' once a full reindex has been done -has associated_pod => ( required => 0, is => 'rw' ); +has indexed => ( + is => 'ro', + required => 1, + isa => ESBool, + default => sub {true}, + writer => '_set_indexed', +); + +has authorized => ( + is => 'ro', + required => 1, + isa => ESBool, + default => sub {true}, + writer => '_set_authorized', +); + +has associated_pod => ( + required => 1, + isa => Maybe [Str], + is => 'ro', + default => sub { }, + writer => '_set_associated_pod', +); + +has version_numified => ( + is => 'ro', + isa => Num, + lazy_build => 1, + required => 1, +); sub _build_version_numified { my $self = shift; @@ -78,20 +108,8 @@ sub _build_version_numified { return MetaCPAN::Util::numify_version( $self->version ); } -sub hide_from_pause { - my ( $self, $content ) = @_; - my $pkg = $self->name; - return $content =~ / # match a package declaration - ^[\h\{;]* # intro chars on a line - package # the word 'package' - \h+ # whitespace - ($pkg) # a package name - \h* # optional whitespace - (.+)? # optional version number - \h* # optional whitesapce - ; # semicolon line terminator - /mx ? 0 : 1; -} +my $bom + = qr/(?:\x00\x00\xfe\xff|\xff\xfe\x00\x00|\xfe\xff|\xff\xfe|\xef\xbb\xbf)/; =head2 set_associated_pod @@ -103,13 +121,49 @@ L is set to the path of the file, which contains the documentat =cut +my %_pod_score = ( + pod => 50, + pm => 40, + pl => 30, +); + sub set_associated_pod { - my ( $self, $file, $associated_pod ) = @_; - if ( my $pod = $associated_pod->{ $self->name } ) { - $self->associated_pod( - join( "/", map { $pod->{$_} } qw(author release path) ) ) - if ( $pod->{path} ne $file->path ); - } + my ( $self, $associated_pod ) = @_; + return unless ( my $files = $associated_pod->{ $self->name } ); + + ( my $mod_path = $self->name ) =~ s{::}{/}g; + + my ($file) = ( + #<<< + # TODO: adjust score if all files are in root? + map { $_->[1] } + sort { $b->[0] <=> $a->[0] } # desc + map { + [ ( + # README.pod in root should rarely if ever be chosen. + # Typically it's there for github or something and it's usually + # a duplicate of the main module pod (though sometimes it falls + # out of sync (which makes it even worse)). + $_->path =~ /^README\.pod$/i ? -10 : + + # If the name of the package matches the name of the file, + $_->path =~ m!(^lib/)?\b${mod_path}.((?i)pod|pm)$! ? + # Score pod over pm, and boost (most points for 'lib' dir). + ($1 ? 50 : 25) + $_pod_score{lc($2)} : + + # Sort files by extension: Foo.pod > Foo.pm > foo.pl. + $_->name =~ /\.(pod|pm|pl)/i ? $_pod_score{lc($1)} : + + # Otherwise score unknown (near the bottom). + -1 + ), + $_ ] + } + @$files + #>>> + ); + $self->_set_associated_pod( $file->full_path ); } __PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Document/Package.pm b/lib/MetaCPAN/Document/Package.pm new file mode 100644 index 000000000..e5ee7bf9f --- /dev/null +++ b/lib/MetaCPAN/Document/Package.pm @@ -0,0 +1,25 @@ +package MetaCPAN::Document::Package; + +use MetaCPAN::Moose; + +use ElasticSearchX::Model::Document; +use MetaCPAN::Types::TypeTiny qw( Str ); + +has module_name => ( + is => 'ro', + isa => Str, + required => 1, +); + +has version => ( + is => 'ro', + isa => Str, +); + +has file => ( + is => 'ro', + isa => Str, +); + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Document/Permission.pm b/lib/MetaCPAN/Document/Permission.pm new file mode 100644 index 000000000..b72524dfd --- /dev/null +++ b/lib/MetaCPAN/Document/Permission.pm @@ -0,0 +1,25 @@ +package MetaCPAN::Document::Permission; + +use MetaCPAN::Moose; + +use ElasticSearchX::Model::Document; +use MetaCPAN::Types::TypeTiny qw( ArrayRef Str ); + +has module_name => ( + is => 'ro', + isa => Str, + required => 1, +); + +has owner => ( + is => 'ro', + isa => Str, +); + +has co_maintainers => ( + is => 'ro', + isa => ArrayRef, +); + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Document/Rating.pm b/lib/MetaCPAN/Document/Rating.pm deleted file mode 100644 index 6335d8429..000000000 --- a/lib/MetaCPAN/Document/Rating.pm +++ /dev/null @@ -1,35 +0,0 @@ -package MetaCPAN::Document::Rating; -use Moose; -use ElasticSearchX::Model::Document; -use ElasticSearchX::Model::Document::Types qw(:all); -use MooseX::Types::Structured qw(Dict Tuple Optional); -use MooseX::Types::Moose qw(Int Num Bool Str ArrayRef HashRef Undef); - -has details => ( is => 'ro', isa => Dict [ documentation => Str ] ); -has rating => - ( required => 1, is => 'ro', isa => Num, builder => '_build_rating' ); -has [qw(distribution release author user)] => - ( required => 1, is => 'ro', isa => Str ); -has date => ( - required => 1, - is => 'ro', - isa => 'DateTime', - default => sub { DateTime->now } -); -has helpful => ( - required => 1, - is => 'ro', - isa => ArrayRef [ Dict [ user => Str, value => Bool ] ], - default => sub { [] } -); - -sub _build_rating { - my $self = shift; - die "Provide details to calculate a rating"; - my %details = %{ $self->details }; - my $rating = 0; - $rating += $_ for ( values %details ); - return $rating / scalar keys %details; -} - -__PACKAGE__->meta->make_immutable; diff --git a/lib/MetaCPAN/Document/Release.pm b/lib/MetaCPAN/Document/Release.pm index 9a2412896..ad92baf61 100644 --- a/lib/MetaCPAN/Document/Release.pm +++ b/lib/MetaCPAN/Document/Release.pm @@ -1,9 +1,19 @@ package MetaCPAN::Document::Release; + use Moose; + use ElasticSearchX::Model::Document; -use MetaCPAN::Document::Author; -use MetaCPAN::Types qw(:all); -use MetaCPAN::Util; +use MetaCPAN::Types qw( Dependency ESBool ); +use MetaCPAN::Types::TypeTiny qw( + ArrayRef + HashRefCPANMeta + Num + Resources + Stat + Str + Tests +); +use MetaCPAN::Util qw( numify_version true false ); =head1 PROPERTIES @@ -32,13 +42,13 @@ PAUSE ID of the author. =head2 archive -Name of the tarball (e.g. C). +Name of the archive file (e.g. C). =head2 date B -Release date (i.e. C of the tarball). +Release date (i.e. C of the archive file). =head2 version @@ -53,9 +63,8 @@ version could not be parsed. Valid values are C, C, and C. The most recent upload of a distribution is tagged as C as long as it's not a developer -release, unless there are only developer releases. Everything else is -tagged C. Once a release is deleted from PAUSE it is tagged as -C. +release. Everything else is tagged C. Once a release is deleted from +PAUSE it is tagged as C. =head2 maturity @@ -71,6 +80,11 @@ See L. See L. +=head2 meta + +See L. Upgraded to version 2 if possible. This property +is not indexed by ElasticSearch and only available from the source. + =head2 abstract Description of the release. @@ -81,24 +95,82 @@ See L. =head2 stat -L info of the tarball. Contains C, C, C, C -and C. +L info of the archive file. Contains C, +C and C. + +=head2 first + +B; Indicates whether this is the first ever release for this distribution. + +=head2 provides + +This is an ArrayRef of modules that are included in this release. + +=cut +=head2 deprecated +This is a boolean indicating whether the release is marked as deprecated +(the main module's ABSTRACT contains "DEPRECATED" or "DEPRECIATED", +or the x_deprecated flag is set in metadata) =cut -has id => ( is => 'ro', id => [qw(author name)] ); -has [qw(license version author archive)] => ( is => 'ro', required => 1 ); -has date => ( is => 'ro', required => 1, isa => 'DateTime' ); -has download_url => ( is => 'ro', required => 1, lazy_build => 1 ); +has provides => ( + is => 'ro', + isa => ArrayRef [Str], + writer => '_set_provides', +); + +has id => ( + is => 'ro', + id => [qw(author name)], +); + +has [qw(version author archive)] => ( + is => 'ro', + required => 1, +); + +has license => ( + is => 'ro', + isa => ArrayRef, + required => 1, +); + +has date => ( + is => 'ro', + required => 1, + isa => 'DateTime', +); + +has download_url => ( + is => 'ro', + lazy => 1, + builder => '_build_download_url', +); + +has [qw(checksum_md5 checksum_sha256)] => ( + is => 'ro', + isa => Str, +); + has [qw(distribution name)] => ( is => 'ro', required => 1, analyzer => [qw(standard camelcase lowercase)], ); -has version_numified => - ( is => 'ro', required => 1, isa => 'Num', lazy_build => 1 ); + +has version_numified => ( + required => 1, + is => 'ro', + isa => Num, + lazy => 1, + default => sub { + return numify_version( shift->version ); + }, +); + has resources => ( is => 'ro', isa => Resources, @@ -107,85 +179,124 @@ has resources => ( type => 'nested', include_in_root => 1, ); -has abstract => ( is => 'rw', index => 'analyzed', predicate => 'has_abstract' ); + +has abstract => ( + is => 'ro', + index => 'analyzed', + predicate => 'has_abstract', + writer => '_set_abstract', +); + has dependency => ( - required => 0, - is => 'rw', + is => 'ro', isa => Dependency, coerce => 1, type => 'nested', include_in_root => 1, ); -has status => ( is => 'rw', required => 1, default => 'cpan' ); -has maturity => ( is => 'ro', required => 1, default => 'released' ); -has stat => ( is => 'ro', isa => Stat, dynamic => 1 ); -has tests => ( is => 'ro', isa => Tests, dynamic => 1 ); -has authorized => ( is => 'rw', required => 1, isa => 'Bool', default => 1 ); + +# The initial state for a release is 'cpan'. +# The indexer scripts will upgrade it to 'latest' if it's the version in +# 02packages or downgrade it to 'backpan' if it gets deleted. +has status => ( + is => 'ro', + required => 1, + default => 'cpan', + writer => '_set_status', +); + +has maturity => ( + is => 'ro', + required => 1, + default => 'released', +); + +has stat => ( + is => 'ro', + isa => Stat, + dynamic => 1, +); + +has tests => ( + is => 'ro', + isa => Tests, + dynamic => 1, + documentation => 'HashRef: Summary of CPANTesters data', +); + +has authorized => ( + is => 'ro', + required => 1, + isa => ESBool, + default => sub {true}, + writer => '_set_authorized', +); + has first => ( is => 'ro', required => 1, - isa => 'Bool', - lazy => 1, - builder => '_build_first' + isa => ESBool, + default => sub {false}, + writer => '_set_first', ); -sub _build_version_numified { - return MetaCPAN::Util::numify_version( shift->version ); -} +has metadata => ( + coerce => 1, + is => 'ro', + isa => HashRefCPANMeta, + dynamic => 1, + source_only => 1, +); + +has main_module => ( + is => 'ro', + isa => Str, + writer => '_set_main_module', +); + +has changes_file => ( + is => 'ro', + isa => Str, + writer => '_set_changes_file', +); + +has deprecated => ( + is => 'ro', + isa => ESBool, + default => sub {false}, + writer => '_set_deprecated', +); sub _build_download_url { my $self = shift; return - 'http://cpan.metacpan.org/authors/' - . MetaCPAN::Document::Author::_build_dir( $self->author ) . '/' + 'https://cpan.metacpan.org/authors/' + . MetaCPAN::Util::author_dir( $self->author ) . '/' . $self->archive; } -sub _build_first { - my $self = shift; - $self->index->type('release') - ->filter( - { term => { 'release.distribution' => $self->distribution } } )->count - ? 0 - : 1; -} - -__PACKAGE__->meta->make_immutable; - -package MetaCPAN::Document::Release::Set; -use Moose; -extends 'ElasticSearchX::Model::Document::Set'; - -sub find_depending_on { - my ( $self, $modules ) = @_; - return $self->filter( - { or => [ - map { { term => { 'release.dependency.module' => $_ } } } @$modules - ] - } - ); -} - -sub find { - my ( $self, $name ) = @_; - return $self->filter( - { and => [ - { term => { 'release.distribution' => $name } }, - { term => { status => 'latest' } } - ] - } - )->sort( [ { date => 'desc' } ] )->first; -} - -sub predecessor { - my ( $self, $name ) = @_; - return $self->filter( - { and => [ - { term => { 'release.distribution' => $name } }, - { not => { filter => { term => { status => 'latest' } } } }, - ] - } - )->sort( [ { date => 'desc' } ] )->first; +sub set_first { + my $self = shift; + my $is_first = $self->index->model->doc('release')->query( { + bool => { + must => [ + { term => { distribution => $self->distribution } }, + { + range => { + version_numified => { lt => $self->version_numified } + } + }, + ], + }, + + # REINDEX: after a full reindex, the above line is to replaced with: + # { term => { first => 1 } }, + # currently, the "first" property is not computed on all releases + # since this feature has not been around when last reindexed + } )->count ? false : true; + + $self->_set_first($is_first); } __PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/ESConfig.pm b/lib/MetaCPAN/ESConfig.pm new file mode 100644 index 000000000..e84f1d928 --- /dev/null +++ b/lib/MetaCPAN/ESConfig.pm @@ -0,0 +1,261 @@ +use v5.20; +use warnings; +use experimental qw( signatures postderef ); + +package MetaCPAN::ESConfig; + +use Carp qw( croak ); +use Const::Fast qw( const ); +use Cpanel::JSON::XS (); +use Exporter qw( import ); +use Hash::Merge::Simple qw( merge ); +use MetaCPAN::Server::Config (); +use MetaCPAN::Types::TypeTiny qw( Defined HashRef ); +use MetaCPAN::Util qw( root_dir true false ); +use Module::Runtime qw( $module_name_rx require_module ); + +const my %config => merge( + { + documents => { + author => { + index => 'author', + type => 'author', + mapping => 'es/author/mapping.json', + settings => 'es/author/settings.json', + model => 'MetaCPAN::Document::Author', + }, + cve => { + index => 'cve', + type => 'cve', + mapping => 'es/cve/mapping.json', + settings => 'es/cve/settings.json', + model => 'MetaCPAN::Document::CVE', + }, + contributor => { + index => 'contributor', + type => 'contributor', + mapping => 'es/contributor/mapping.json', + settings => 'es/contributor/settings.json', + model => 'MetaCPAN::Document::Contributor', + }, + cover => { + index => 'cover', + type => 'cover', + mapping => 'es/cover/mapping.json', + settings => 'es/cover/settings.json', + model => 'MetaCPAN::Document::Cover', + }, + distribution => { + index => 'distribution', + type => 'distribution', + mapping => 'es/distribution/mapping.json', + settings => 'es/distribution/settings.json', + model => 'MetaCPAN::Document::Distribution', + }, + favorite => { + index => 'favorite', + type => 'favorite', + mapping => 'es/favorite/mapping.json', + settings => 'es/favorite/settings.json', + model => 'MetaCPAN::Document::Favorite', + }, + file => { + index => 'file', + type => 'file', + mapping => 'es/file/mapping.json', + settings => 'es/file/settings.json', + model => 'MetaCPAN::Document::File', + }, + mirror => { + index => 'mirror', + type => 'mirror', + mapping => 'es/mirror/mapping.json', + settings => 'es/mirror/settings.json', + model => 'MetaCPAN::Document::Mirror', + }, + package => { + index => 'package', + type => 'package', + mapping => 'es/package/mapping.json', + settings => 'es/package/settings.json', + model => 'MetaCPAN::Document::Package', + }, + permission => { + index => 'permission', + type => 'permission', + mapping => 'es/permission/mapping.json', + settings => 'es/permission/settings.json', + model => 'MetaCPAN::Document::Permission', + }, + release => { + index => 'release', + type => 'release', + mapping => 'es/release/mapping.json', + settings => 'es/release/settings.json', + model => 'MetaCPAN::Document::Release', + }, + + account => { + index => 'account', + type => 'account', + mapping => 'es/account/mapping.json', + settings => 'es/account/settings.json', + model => 'MetaCPAN::Model::User::Account', + }, + session => { + index => 'session', + type => 'session', + mapping => 'es/session/mapping.json', + settings => 'es/session/settings.json', + model => 'MetaCPAN::Model::User::Session', + }, + }, + }, + MetaCPAN::Server::Config::config()->{elasticsearch} || {}, +)->%*; + +{ + use Moo; +} + +has all_indexes => ( + is => 'lazy', + default => sub ($self) { + my %seen; + [ + sort + grep !$seen{$_}++, + map $_->{index}, + values $self->documents->%* + ]; + }, +); + +my $DefinedHash = ( HashRef [Defined] )->plus_coercions( + HashRef, + => sub ($hash) { + return { + map { + my $value = $hash->{$_}; + defined $value ? ( $_ => $value ) : (); + } keys %$hash + }; + }, +); + +has documents => ( + is => 'ro', + isa => HashRef [$DefinedHash], + coerce => 1, + required => 1, +); + +sub _load_es_data ( $location, $def_sub ) { + my $data; + + if ( ref $location ) { + $data = $location; + } + elsif ( $location + =~ /\A($module_name_rx)(?:::([0-9a-zA-Z_]+)\(\)|->($module_name_rx))?\z/ + ) + { + my ( $module, $sub, $method ) = ( $1, $2, $3 ); + require_module $module; + if ($method) { + $data = $module->$method; + } + else { + $sub ||= $def_sub; + no strict 'refs'; + my $code = \&{"${module}::${sub}"}; + die "can't find $location" + if !defined &$code; + $data = $code->(); + } + } + else { + my $abs_path = File::Spec->rel2abs( $location, root_dir() ); + open my $fh, '<', $abs_path + or die "can't open mapping file $abs_path: $!"; + $data = do { local $/; <$fh> }; + } + + return $data + if ref $data; + + return Cpanel::JSON::XS::decode_json($data); +} + +sub _walk : prototype(&$); + +sub _walk : prototype(&$) { + my ( $cb, $data ) = @_; + if ( ref $data eq 'HASH' ) { + $cb->($data); + _walk( \&$cb, $data->{$_} ) for keys %$data; + } + elsif ( ref $data eq 'ARRAY' ) { + $cb->($data); + _walk( \&$cb, $_ ) for @$data; + } +} + +sub mapping ( $self, $doc, $version ) { + my $doc_data = $self->documents->{$doc} + or croak "unknown document $doc"; + my $data = _load_es_data( $doc_data->{mapping}, 'mapping' ); + if ( $version && $version eq '2_0' ) { + _walk( + sub { + my ($d) = @_; + if ( my $type = $d->{type} ) { + if ( $type eq 'keyword' ) { + $d->{type} = 'string'; + $d->{index} = 'not_analyzed'; + $d->{ignore_above} = 2048; + } + elsif ( $type eq 'text' ) { + $d->{type} = 'string'; + if ( exists $d->{fielddata} && !$d->{fielddata} ) { + $d->{fielddata} = { format => false }; + } + } + } + + }, + $data + ); + } + return $data; +} + +sub index_settings ( $self, $doc, $version = undef ) { + my $documents = $self->documents; + my $doc_data = exists $documents->{$doc} && $documents->{$doc} + or return {}; + my $settings = exists $doc_data->{settings} && $doc_data->{settings} + or return {}; + my $data = _load_es_data( $settings, 'settings' ); + return $data; +} + +sub doc_path ( $self, $doc ) { + my $doc_data = $self->documents->{$doc} + or croak "unknown document $doc"; + return ( + ( $doc_data->{index} ? ( index => $doc_data->{index} ) : () ), + ( $doc_data->{type} ? ( type => $doc_data->{type} ) : () ), + ); +} + +our @EXPORT_OK = qw( + es_config + es_doc_path +); + +my $single = __PACKAGE__->new(%config); +sub es_config : prototype() {$single} +sub es_doc_path ($doc) { $single->doc_path($doc) } + +1; diff --git a/lib/MetaCPAN/Model.pm b/lib/MetaCPAN/Model.pm index 740c3aa0d..e98d5ac6e 100644 --- a/lib/MetaCPAN/Model.pm +++ b/lib/MetaCPAN/Model.pm @@ -1,23 +1,38 @@ package MetaCPAN::Model; + +# load order important use Moose; + use ElasticSearchX::Model; +use MetaCPAN::ESConfig qw(es_config); +use Module::Runtime qw( require_module use_package_optimistically ); + +my %indexes; +my $docs = es_config->documents; +for my $name ( sort keys %$docs ) { + my $doc = $docs->{$name}; + my $model = $doc->{model} + or next; + require_module($model); + use_package_optimistically( $model . '::Set' ); + my $index = $doc->{index} + or die "no index for $name documents!"; -analyzer lowercase => ( tokenizer => 'keyword', filter => 'lowercase' ); -analyzer fulltext => ( type => 'snowball', language => 'English' ); -tokenizer camelcase => ( - type => 'pattern', - pattern => "([^\\p{L}\\d]+)|(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)|(?<=[\\p{L}&&[^\\p{Lu}]])(?=\\p{Lu})|(?<=\\p{Lu})(?=\\p{Lu}[\\p{L}&&[^\\p{Lu}]])" -); -analyzer camelcase => ( - type => 'custom', - tokenizer => 'camelcase', - filter => ['lowercase', 'unique'] -); + $indexes{$index}{types}{$name} = $model->meta; +} -index cpan => ( namespace => 'MetaCPAN::Document', alias_for => 'cpan_v1', shards => 5 ); +for my $index ( sort keys %indexes ) { + index $index => %{ $indexes{$index} }; +} -index user => ( namespace => 'MetaCPAN::Model::User' ); +sub doc { + my ( $self, $doc ) = @_; + my $doc_config = es_config->documents->{$doc}; + return $self->index( $doc_config->{index} ) + ->type( $doc_config->{type} // $doc_config->{index} ); +} __PACKAGE__->meta->make_immutable; +1; __END__ diff --git a/lib/MetaCPAN/Model/Archive.pm b/lib/MetaCPAN/Model/Archive.pm new file mode 100644 index 000000000..3ad4eebae --- /dev/null +++ b/lib/MetaCPAN/Model/Archive.pm @@ -0,0 +1,174 @@ +package MetaCPAN::Model::Archive; + +use v5.10; +use Moose; +use MooseX::StrictConstructor; +use MetaCPAN::Types::TypeTiny qw( AbsPath ArrayRef Bool InstanceOf Str ); + +use Archive::Any (); +use Carp qw( croak ); +use Digest::file qw( digest_file_hex ); +use Path::Tiny qw( path ); + +=head1 NAME + +MetaCPAN::Model::Archive - Inspect and extract archive files + +=head1 SYNOPSIS + + use MetaCPAN::Model::Archive; + + my $archive = MetaCPAN::Model::Archive->new( file => $some_file ); + my $files = $archive->files; + my $extraction_dir = $archive->extract; + +=head1 DESCRIPTION + +This class manages getting information about and extraction of archive +files (tarballs, zipfiles, etc...) and their extraction directories. + +The object is read-only and will only extract once. If you alter the +extraction directory and want a fresh one, make a new object. + +The Archive will clean up its extraction directory upon destruction. + +=head1 ATTRIBUTES + +=head3 archive + +I + +The file to be extracted. It will be returned as a Path::Tiny +object. + +=cut + +has file => ( + is => 'ro', + isa => AbsPath, + coerce => 1, + required => 1, +); + +has _extractor => ( + is => 'ro', + isa => InstanceOf ['Archive::Any'], + handles => [ qw( + is_impolite + is_naughty + ) ], + init_arg => undef, + lazy => 1, + default => sub { + my $self = shift; + croak $self->file . ' does not exist' unless -e $self->file; + return Archive::Any->new( $self->file ); + }, +); + +# MD5 digest for the archive file +has file_digest_md5 => ( + is => 'ro', + isa => Str, + lazy => 1, + default => sub { + my $self = shift; + digest_file_hex( $self->file, 'MD5' ); + }, +); + +# SHA256 digest for the archive file +has file_digest_sha256 => ( + is => 'ro', + isa => Str, + lazy => 1, + default => sub { + my $self = shift; + digest_file_hex( $self->file, 'SHA-256' ); + }, +); + +# Holding the File::Temp::Dir object here is necessary to keep it +# alive until the object is destroyed. +has _tempdir => ( + is => 'ro', + isa => AbsPath, + init_arg => undef, + lazy => 1, + default => sub { + + my $scratch_disk = '/mnt/scratch_disk'; + return -d $scratch_disk + ? Path::Tiny->tempdir('/mnt/scratch_disk/tempXXXXX') + : Path::Tiny->tempdir; + }, +); + +has extract_dir => ( + is => 'ro', + isa => AbsPath, + lazy => 1, + coerce => 1, + default => sub { + my $self = shift; + return path( $self->_tempdir ); + }, +); + +has _has_extracted => ( + is => 'ro', + isa => Bool, + init_arg => undef, + default => 0, + writer => '_set_has_extracted', +); + +=head1 METHODS + +=head3 files + + my $files = $archive->files; + +A list of the files in the archive as an array ref. + +=cut + +# A cheap way to cache the result. +has files => ( + is => 'ro', + isa => ArrayRef, + init_arg => undef, + lazy => 1, + default => sub { + my $self = shift; + return [ $self->_extractor->files ]; + }, +); + +=head3 extract + + my $extract_dir = $archive->extract; + +Extract the archive into a temp directory. The directory will be a +L object. + +Only the first call to extract will perform the extraction. After +that it will just return the extraction directory. If you want to +re-extract the archive, create a new object. + +The extraction directory will be cleaned up when the object is destroyed. + +=cut + +sub extract { + my $self = shift; + + return $self->extract_dir if $self->_has_extracted; + + $self->_extractor->extract( $self->extract_dir ); + $self->_set_has_extracted(1); + + return $self->extract_dir; +} + +1; diff --git a/lib/MetaCPAN/Model/ESWrapper.pm b/lib/MetaCPAN/Model/ESWrapper.pm new file mode 100644 index 000000000..cd990c897 --- /dev/null +++ b/lib/MetaCPAN/Model/ESWrapper.pm @@ -0,0 +1,61 @@ +package MetaCPAN::Model::ESWrapper; +use strict; +use warnings; + +use MetaCPAN::Types::TypeTiny qw( ES ); + +sub new { + my ( $class, $es ) = @_; + if ( $es->api_version le '6_0' ) { + return $es; + } + return bless { es => ES->assert_coerce($es) }, $class; +} + +sub DESTROY { } + +sub AUTOLOAD { + my $sub = our $AUTOLOAD =~ s/.*:://r; + my $self = shift; + $self->{es}->$sub(@_); +} + +sub _args { + my $self = shift; + if ( @_ == 1 ) { + return ( $self, %{ $_[0] } ); + } + return ( $self, @_ ); +} + +sub count { + my ( $self, %args ) = &_args; + delete $args{type}; + $self->{es}->count(%args); +} + +sub get { + my ( $self, %args ) = &_args; + delete $args{type}; + $self->{es}->get(%args); +} + +sub delete { + my ( $self, %args ) = &_args; + delete $args{type}; + $self->{es}->delete(%args); +} + +sub search { + my ( $self, %args ) = &_args; + delete $args{type}; + $self->{es}->search(%args); +} + +sub scroll_helper { + my ( $self, %args ) = &_args; + delete $args{type}; + $self->{es}->scroll_helper(%args); +} + +1; diff --git a/lib/MetaCPAN/Model/Email/PAUSE.pm b/lib/MetaCPAN/Model/Email/PAUSE.pm new file mode 100644 index 000000000..b022dcf0d --- /dev/null +++ b/lib/MetaCPAN/Model/Email/PAUSE.pm @@ -0,0 +1,90 @@ +package MetaCPAN::Model::Email::PAUSE; + +use MetaCPAN::Moose; + +use Email::Sender::Simple qw( sendmail ); +use Email::Sender::Transport::SMTP (); +use Email::Simple (); +use Encode (); +use MetaCPAN::Types::TypeTiny qw( Object Uri ); +use Try::Tiny qw( catch try ); + +with('MetaCPAN::Role::HasConfig'); + +has _author => ( + is => 'ro', + isa => Object, + init_arg => 'author', + required => 1, +); + +has _url => ( + is => 'ro', + isa => Uri, + init_arg => 'url', + required => 1, +); + +sub send { + my $self = shift; + + my $email = Email::Simple->create( + header => [ + 'Content-Type' => 'text/plain; charset=utf-8', + To => $self->_author->{email}->[0], + From => 'notifications@metacpan.org', + Subject => 'Connect MetaCPAN with your PAUSE account', + 'MIME-Version' => '1.0', + ], + body => $self->_email_body, + ); + + my $config = $self->config->{smtp}; + my $transport = Email::Sender::Transport::SMTP->new( { + debug => 1, + host => $config->{host}, + port => $config->{port}, + sasl_username => $config->{username}, + sasl_password => $config->{password}, + ssl => 1, + } ); + + my $success = 0; + try { + $success = sendmail( $email, { transport => $transport } ); + } + catch { + warn 'Could not send message: ' . $_; + }; + + return $success; +} + +sub _email_body { + my $self = shift; + my $name = $self->_author->name; + my $uri = $self->_url; + + my $body = < ( + is => 'ro', + isa => InstanceOf ['MetaCPAN::Model::Archive'], + lazy => 1, + builder => '_build_archive', +); + +has dependencies => ( + is => 'ro', + isa => ArrayRef, + lazy => 1, + builder => '_build_dependencies', +); + +has distinfo => ( + is => 'ro', + isa => InstanceOf ['CPAN::DistnameInfo'], + handles => { + maturity => 'maturity', + author => 'cpanid', + name => 'distvname', + distribution => 'dist', + filename => 'filename', + }, + lazy => 1, + default => sub { + my $self = shift; + return CPAN::DistnameInfo->new( $self->file ); + }, +); + +has document => ( + is => 'ro', + isa => InstanceOf ['MetaCPAN::Document::Release'], + lazy => 1, + builder => '_build_document', +); + +has file => ( + is => 'ro', + isa => AbsPath, + required => 1, + coerce => 1, +); + +has files => ( + is => 'ro', + isa => ArrayRef, + init_arg => undef, + lazy => 1, + builder => '_build_files', +); + +has date => ( + is => 'ro', + isa => InstanceOf ['DateTime'], + lazy => 1, + default => sub { + my $self = shift; + return DateTime->from_epoch( epoch => $self->file->stat->mtime ); + }, +); + +has model => ( is => 'ro' ); + +has metadata => ( + is => 'ro', + isa => InstanceOf ['CPAN::Meta'], + lazy => 1, + builder => '_build_metadata', +); + +has modules => ( + is => 'ro', + isa => ArrayRef, + lazy => 1, + default => sub { + my $self = shift; + if ( keys %{ $self->metadata->provides } ) { + return $self->_modules_from_meta; + } + else { + return $self->_modules_from_files; + } + }, +); + +has version => ( + is => 'ro', + isa => Str, + lazy => 1, + default => sub { + return fix_version( shift->distinfo->version ); + }, +); + +has status => ( + is => 'ro', + isa => Str, +); + +has es => ( is => 'ro' ); +has bulk => ( is => 'ro' ); + +=head2 run + +Try to fix some ordering issues, which are causing deep recursion. There's +probably a much cleaner way to do this. + +=cut + +sub run { + my $self = shift; + $self->document; + $self->document->_set_changes_file( + $self->get_changes_file( $self->files ) ); + $self->set_main_module( $self->modules, $self->document ); +} + +sub _build_archive { + my $self = shift; + + log_info { 'Processing ', $self->file }; + + my $archive = MetaCPAN::Model::Archive->new( file => $self->file ); + + log_error { $self->file, ' is being impolite' } if $archive->is_impolite; + + log_error { $self->file, ' is being naughty' } if $archive->is_naughty; + + return $archive; +} + +sub _build_dependencies { + my $self = shift; + my $meta = $self->metadata; + + log_debug {'Gathering dependencies'}; + + my @dependencies; + if ( my $prereqs = $meta->prereqs ) { + while ( my ( $phase, $data ) = each %$prereqs ) { + while ( my ( $relationship, $v ) = each %$data ) { + while ( my ( $module, $version ) = each %$v ) { + push( + @dependencies, + Dlog_trace {"adding dependency $_"} +{ + phase => $phase, + relationship => $relationship, + module => $module, + version => $version, + } + ); + } + } + } + } + + log_debug { 'Found ', scalar @dependencies, ' dependencies' }; + + return \@dependencies; +} + +sub _build_document { + my $self = shift; + + my $st = $self->file->stat; + my $stat = { map { $_ => $st->$_ } qw(mode size mtime) }; + + my $meta = $self->metadata; + my $dependencies = $self->dependencies; + + my $document = DlogS_trace {"adding release $_"} +{ + abstract => MetaCPAN::Util::strip_pod( $meta->abstract ), + archive => $self->filename, + author => $self->author, + checksum_md5 => $self->archive->file_digest_md5, + checksum_sha256 => $self->archive->file_digest_sha256, + date => $self->date . q{}, + dependency => $dependencies, + distribution => $self->distribution, + + # CPAN::Meta->license *must* be called in list context + # (and *may* return multiple strings). + license => [ $meta->license ], + maturity => $self->maturity, + metadata => $meta, + name => $self->name, + provides => [], + stat => $stat, + status => $self->status, + +# Call in scalar context to make sure we only get one value (building a hash). + ( map { ( $_ => scalar $meta->$_ ) } qw( version resources ) ), + }; + + delete $document->{abstract} + if ( $document->{abstract} eq 'unknown' + || $document->{abstract} eq 'null' ); + + $document + = $self->model->doc('release')->put( $document, { refresh => true } ); + + # create distribution if doesn't exist + $self->es->update( + es_doc_path('distribution'), + id => $self->distribution, + body => { + doc => { + name => $self->distribution, + }, + doc_as_upsert => true, + }, + ); + + return $document; +} + +sub set_main_module { + my $self = shift; + my ( $mod, $release ) = @_; + + # Only select modules (files) that have modules (packages). + my @modules = grep { scalar @{ $_->module } } @$mod; + + return unless @modules; + + my $dist2module = $release->distribution; + $dist2module =~ s{-}{::}g; + + if ( scalar @modules == 1 ) { + + # there is only one module and it will become the main_module + $release->_set_main_module( $modules[0]->module->[0]->name ); + return; + } + + foreach my $file (@modules) { + + # the module has the exact name as the ditribution + if ( $file->module->[0]->name eq $dist2module ) { + $release->_set_main_module( $file->module->[0]->name ); + return; + } + } + + # the distribution has modules on different levels + # the main_module is the first one with the minimum level + # or if they are on the same level, the one with the shortest name + my @sorted_modules = sort { + $a->level <=> $b->level + || length $a->module->[0]->name <=> length $b->module->[0]->name + } @modules; + $release->_set_main_module( $sorted_modules[0]->module->[0]->name ); + +} + +my @changes_files = qw( + CHANGELOG + ChangeLog + Changelog + CHANGES + Changes + NEWS +); +my @exclude_dirs = qw( + corpus + fatlib + inc + local + perl5 + share + t + xt +); + +# this should match the same set of files as MetaCPAN::Query::File->interesting_files +my ($changes_match) = map qr/^(?:$_)$/, join '|', + ( map quotemeta, @changes_files, map "$_.md", @changes_files ), + ( + "(?:(?!" + . join( '|', map "$_/", @exclude_dirs ) + . ").*/)?(?:" + . join( + '|', map quotemeta, map +( "$_.pm", "$_.pod" ), @changes_files + ) + . ')' + ); + +sub get_changes_file { + my $self = shift; + my @files = @{ $_[0] }; + if ( $files[0]->distribution eq 'perl' ) { + foreach my $file (@files) { + if ( $file->name eq 'perldelta.pod' ) { + return $file->path; + } + } + } + + # prioritize files in the top level but otherwise alphabetical + @files = sort { $a->level <=> $b->level || $a->path cmp $b->path } @files; + + foreach my $file (@files) { + return $file->path if $file->path =~ $changes_match; + } +} + +sub _build_files { + my $self = shift; + + my @files; + log_debug { 'Indexing ', scalar @{ $self->archive->files }, ' files' }; + my $file_set = $self->model->doc('file'); + + my $extract_dir = $self->extract; + File::Find::find( + sub { + my $child = path($File::Find::name); + return if $self->_is_broken_file($File::Find::name); + my $relative = $child->relative($extract_dir); + my $stat = do { + my $s = $child->stat; + +{ map { $_ => $s->$_ } qw(mode size mtime) }; + }; + return if ( $relative eq q{.} ); + ( my $fpath = "$relative" ) =~ s/^.*?\///; + my $filename = $fpath; + $child->is_dir + ? $filename =~ s/^(.*\/)?(.+?)\/?$/$2/ + : $filename =~ s/.*\///; + $fpath = q{} if $relative !~ /\// && !$self->archive->is_impolite; + + my $file = $file_set->new_document( + Dlog_trace {"adding file $_"} +{ + author => $self->author, + binary => -B $child ? true : false, + content => $child->is_dir ? \"" + : \( scalar $child->slurp ), + date => $self->date, + directory => $child->is_dir ? true : false, + distribution => $self->distribution, + indexed => $self->metadata->should_index_file($fpath) + ? true + : false, + local_path => $child, + maturity => $self->maturity, + metadata => $self->metadata, + name => $filename, + path => $fpath, + release => $self->name, + download_url => $self->document->download_url, + stat => $stat, + status => $self->status, + version => $self->version, + } + ); + + $self->bulk->put($file); + push( @files, $file ); + }, + $extract_dir + ); + + $self->bulk->commit; + + return \@files; +} + +my @always_no_index_dirs = ( + + # Always ignore the same dirs as PAUSE (lib/PAUSE/dist.pm): + ## skip "t" - libraries in ./t are test libraries! + ## skip "xt" - libraries in ./xt are author test libraries! + ## skip "inc" - libraries in ./inc are usually install libraries + ## skip "local" - somebody shipped his carton setup! + ## skip 'perl5" - somebody shipped her local::lib! + ## skip 'fatlib' - somebody shipped their fatpack lib! + qw( t xt inc local perl5 fatlib ), + + # and add a few more + qw( example blib examples eg ), +); + +sub _build_metadata { + my $self = shift; + + my $extract_dir = $self->extract; + + return $self->_load_meta_file || CPAN::Meta->new( { + license => 'unknown', + name => $self->distribution, + no_index => { directory => [@always_no_index_dirs] }, + version => $self->version || 0, + } ); +} + +sub _load_meta_file { + my $self = shift; + + my $extract_dir = $self->extract; + + my @files; + for (qw{*/META.json */META.yml */META.yaml META.json META.yml META.yaml}) + { + + # scalar context globbing (without exhausting results) produces + # confusing results (which caused existsing */META.json files to + # get skipped). using list context seems more reliable. + my ($path) = <$extract_dir/$_>; + push( @files, $path ) if ( $path && -e $path ); + } + return unless (@files); + + my $last; + for my $file (@files) { + try { + $last = CPAN::Meta->load_file($file); + } + catch { + log_warn {"META file ($file) could not be loaded: $_"}; + }; + if ($last) { + last; + } + } + if ($last) { + push( @{ $last->{no_index}->{directory} }, @always_no_index_dirs ); + return $last; + } + + log_warn {'No META files could be loaded'}; +} + +sub extract { + my $self = shift; + + log_debug {'Extracting archive to filesystem'}; + return $self->archive->extract; +} + +sub _is_broken_file { + my $self = shift; + my $filename = shift; + + return 1 if ( -p $filename || !-e $filename ); + + if ( -l $filename ) { + my $syml = readlink $filename; + return 1 if ( !-e $filename && !-l $filename ); + } + return 0; +} + +sub _modules_from_meta { + my $self = shift; + + my @modules; + + my $provides = $self->metadata->provides; + my $files = $self->files; + my %files = map +( $_->path => $_ ), @$files; + foreach my $module ( sort keys %$provides ) { + my $data = $provides->{$module}; + my $path = File::Spec->canonpath( $data->{file} ); + + my $file = $files{$path} + or next; + + next unless $file->indexed; + + $file->add_module( { + name => $module, + version => $data->{version}, + indexed => true, + } ); + push( @modules, $file ); + } + + return \@modules; +} + +sub _modules_from_files { + my $self = shift; + + my @modules; + + my @perl_files = grep { $_->name =~ m{(?:\.pm|\.pm\.PL)\z} } + grep { $_->indexed } @{ $self->files }; + foreach my $file (@perl_files) { + if ( $file->name =~ m{\.PL\z} ) { + my $parser = Parse::PMFile->new( $self->metadata->as_struct ); + + # FIXME: Should there be a timeout on this + # (like there is below for Module::Metadata)? + my $info = $parser->parse( $file->local_path ); + next if !$info; + + foreach my $module_name ( keys %{$info} ) { + $file->add_module( { + name => $module_name, + defined $info->{$module_name}->{version} + ? ( version => $info->{$module_name}->{version} ) + : (), + } ); + } + push @modules, $file; + } + else { + eval { + local $SIG{'ALRM'} = sub { + log_error {'Call to Module::Metadata timed out '}; + die; + }; + alarm(50); + my $info; + { + local $SIG{__WARN__} = sub { }; + $info = Module::Metadata->new_from_file( + $file->local_path ); + } + + # Ignore packages that people cannot claim. + # https://github.com/andk/pause/blob/master/lib/PAUSE/pmfile.pm#L236 + # + # Parse::PMFile and PAUSE translate apostrophes to double colons, + # but Module::Metadata does not. + my @packages + = map s{'}{::}gr, + grep { $_ ne 'main' && $_ ne 'DB' } + $info->packages_inside; + + for my $pkg (@packages) { + my $version = $info->version($pkg); + $file->add_module( { + name => $pkg, + defined $version + +# Stringify if it's a version object, otherwise fall back to stupid stringification +# Changes in Module::Metadata were causing inconsistencies in the return value, +# we are just trying to survive. + ? ( + version => ref $version eq 'version' + ? $version->stringify + : ( $version . q{} ) + ) + : () + } ); + } + push( @modules, $file ); + alarm(0); + }; + } + } + + return \@modules; +} + +__PACKAGE__->meta->make_immutable(); +1; diff --git a/lib/MetaCPAN/Model/User/Account.pm b/lib/MetaCPAN/Model/User/Account.pm index 28ed57f9f..bfb37ce31 100644 --- a/lib/MetaCPAN/Model/User/Account.pm +++ b/lib/MetaCPAN/Model/User/Account.pm @@ -1,13 +1,34 @@ package MetaCPAN::Model::User::Account; + +use strict; +use warnings; + use Moose; use ElasticSearchX::Model::Document; -use MetaCPAN::Model::User::Identity; -use MetaCPAN::Util; -use MooseX::Types::Structured qw(Dict); -use MooseX::Types::Moose qw(Str ArrayRef); -use MetaCPAN::Types qw(:all); -has id => ( id => 1, required => 0, is => 'rw' ); +use MetaCPAN::Types qw( Identity ); +use MetaCPAN::Types::TypeTiny qw( ArrayRef Dict Str ); +use MetaCPAN::Util qw(true false); + +=head1 PROPERTIES + +=head2 id + +ID of user account. + +=cut + +has id => ( + id => 1, + is => 'ro', +); + +=head2 identity + +Array of L objects. Each identity is a +authentication provider such as Twitter or GitHub. + +=cut has identity => ( is => 'ro', @@ -16,84 +37,96 @@ has identity => ( coerce => 1, traits => ['Array'], handles => { add_identity => 'push' }, - default => sub { [] } + default => sub { [] }, ); -has code => ( is => 'rw', clearer => 'clear_token' ); +=head2 code + +The code attribute is used temporarily when authenticating using OAuth. + +=cut + +has code => ( + is => 'ro', + clearer => 'clear_token', + writer => '_set_code', +); + +=head2 access_token + +Array of access token that allow third-party applications to authenticate +as the user. + +=cut has access_token => ( is => 'ro', required => 1, isa => ArrayRef [ Dict [ token => Str, client => Str ] ], - default => sub { [] }, - dynamic => 1, - traits => ['Array'], - handles => { add_access_token => 'push' } + default => sub { [] }, + dynamic => 1, + traits => ['Array'], + handles => { add_access_token => 'push' }, ); -has passed_captcha => ( is => 'rw', isa => 'DateTime' ); +=head1 METHODS -has looks_human => ( - is => 'ro', - isa => 'Bool', - required => 1, - lazy_build => 1, - clearer => 'clear_looks_human' -); +=head2 add_identity + +Adds an identity to L. If the identity is a PAUSE account, +the user ID is added to the corresponding L document. + +=cut after add_identity => sub { my ( $self, $identity ) = @_; if ( $identity->{name} eq 'pause' ) { - $self->clear_looks_human; - my $profile = $self->index->model->index('cpan')->type('author') - ->get( $identity->{key} ); - $profile->user( $self->id ) if($profile); - $profile->put; + my $profile + = $self->index->model->doc('author')->get( $identity->{key} ); + + # Not every user is an author + if ($profile) { + $profile->_set_user( $self->id ); + $profile->put; + } } }; -sub _build_looks_human { - my $self = shift; - return $self->has_identity('pause') || $self->passed_captcha; -} +=head2 has_identity + +=cut sub has_identity { my ( $self, $identity ) = @_; return scalar grep { $_->name eq $identity } @{ $self->identity }; } +=head2 get_identities + +=cut + sub get_identities { my ( $self, $identity ) = @_; return grep { $_->name eq $identity } @{ $self->identity }; } -__PACKAGE__->meta->make_immutable; +sub remove_identity { + my ( $self, $identity ) = @_; + my $ids = $self->identity; + my ($id) = grep { $_->{name} eq $identity } @$ids; + @$ids = grep { $_->{name} ne $identity } @$ids; -package MetaCPAN::Model::User::Account::Set; + if ( $identity eq 'pause' ) { + my $profile = $self->index->model->doc('author')->get( $id->{key} ); -use Moose; -extends 'ElasticSearchX::Model::Document::Set'; - -sub find { - my ( $self, $p ) = @_; - return $self->filter( - { and => [ - { term => { 'account.identity.name' => $p->{name} } }, - { term => { 'account.identity.key' => $p->{key} } } - ] + if ( $profile && $profile->user eq $self->id ) { + $profile->_clear_user; + $profile->put; } - )->first; -} - -sub find_code { - my ( $self, $token ) = @_; - return $self->filter( { term => { 'account.code' => $token } } )->first; -} + } -sub find_token { - my ( $self, $token ) = @_; - return $self->filter( - { term => { 'account.access_token.token' => $token } } )->first; + return $id; } __PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Model/User/Account/Set.pm b/lib/MetaCPAN/Model/User/Account/Set.pm new file mode 100644 index 000000000..6891aacf1 --- /dev/null +++ b/lib/MetaCPAN/Model/User/Account/Set.pm @@ -0,0 +1,56 @@ +package MetaCPAN::Model::User::Account::Set; + +use Moose; +extends 'ElasticSearchX::Model::Document::Set'; + +=head1 SET METHODS + +=head2 find + + $type->find({ name => "github", key => 123455 }); + +Find an account based on its identity. + +=cut + +sub find { + my ( $self, $p ) = @_; + return $self->query( { + bool => { + must => [ + { term => { 'identity.name' => $p->{name} } }, + { term => { 'identity.key' => $p->{key} } }, + ], + } + } )->first; +} + +=head2 find_code + + $type->find_code($code); + +Find account by C<$code>. See L. + +=cut + +sub find_code { + my ( $self, $token ) = @_; + return $self->query( { term => { code => $token } } )->first; +} + +=head2 find_token + + $type->find_token($access_token); + +Find account by C<$access_token>. See L. + +=cut + +sub find_token { + my ( $self, $token ) = @_; + return $self->query( { term => { 'access_token.token' => $token } } ) + ->first; +} + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Model/User/Identity.pm b/lib/MetaCPAN/Model/User/Identity.pm index 403a5ccae..6236b31a2 100644 --- a/lib/MetaCPAN/Model/User/Identity.pm +++ b/lib/MetaCPAN/Model/User/Identity.pm @@ -1,11 +1,25 @@ package MetaCPAN::Model::User::Identity; + +use strict; +use warnings; + use Moose; use ElasticSearchX::Model::Document; +use MetaCPAN::Types::TypeTiny qw( HashRef ); -has name => ( is => 'ro', required => 1 ); +has name => ( + is => 'ro', + required => 1, +); has key => ( is => 'ro' ); -has extra => ( is => 'ro', isa => 'HashRef', source_only => 1, dynamic => 1 ); +has extra => ( + is => 'ro', + isa => HashRef, + source_only => 1, + dynamic => 1, +); __PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Model/User/Session.pm b/lib/MetaCPAN/Model/User/Session.pm index 6893f4dd2..377068a55 100644 --- a/lib/MetaCPAN/Model/User/Session.pm +++ b/lib/MetaCPAN/Model/User/Session.pm @@ -1,13 +1,10 @@ package MetaCPAN::Model::User::Session; -use Moose; -use ElasticSearchX::Model::Document; -use DateTime; - -has id => ( is => 'ro', id => 1 ); -has date => - ( is => 'ro', required => 1, isa => 'DateTime', default => sub { DateTime->now } ); +use strict; +use warnings; -has account => ( parent => 1, is => 'rw', required => 1 ); +use Moose; +use ElasticSearchX::Model::Document; __PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Pod/Renderer.pm b/lib/MetaCPAN/Pod/Renderer.pm new file mode 100644 index 000000000..6cb0591dd --- /dev/null +++ b/lib/MetaCPAN/Pod/Renderer.pm @@ -0,0 +1,100 @@ +package MetaCPAN::Pod::Renderer; + +use MetaCPAN::Moose; + +use MetaCPAN::Pod::HTML; +use MetaCPAN::Types::TypeTiny qw( Uri ); +use Pod::Markdown (); +use Pod::Simple::JustPod (); +use Pod::Text (); + +has perldoc_url_prefix => ( + is => 'ro', + isa => Uri, + coerce => 1, + default => 'https://metacpan.org/pod/', + writer => '_set_perldoc_url_prefix', +); + +has nix_X_codes => ( is => 'ro' ); + +has no_errata_section => ( + is => 'ro', + default => 1, +); + +has link_mappings => ( is => 'ro' ); + +sub markdown_renderer { + my $self = shift; + return Pod::Markdown->new( + perldoc_url_prefix => $self->perldoc_url_prefix ); +} + +sub pod_renderer { + my $self = shift; + return Pod::Simple::JustPod->new; +} + +sub text_renderer { + my $self = shift; + return Pod::Text->new( sentence => 0, width => 78 ); +} + +sub html_renderer { + my $self = shift; + + my $parser = MetaCPAN::Pod::HTML->new; + + $parser->html_footer(''); + $parser->html_header(''); + $parser->index(1); + $parser->no_errata_section( $self->no_errata_section ); + $parser->perldoc_url_prefix( $self->perldoc_url_prefix ); + $parser->link_mappings( $self->link_mappings ); + + return $parser; +} + +sub to_markdown { + my $self = shift; + my $source = shift; + + return $self->_generic_render( $self->markdown_renderer, $source ); +} + +sub to_text { + my $self = shift; + my $source = shift; + + return $self->_generic_render( $self->text_renderer, $source ); +} + +sub to_html { + my $self = shift; + my $source = shift; + + return $self->_generic_render( $self->html_renderer, $source ); +} + +sub to_pod { + my $self = shift; + my $source = shift; + + return $self->_generic_render( $self->pod_renderer, $source ); +} + +sub _generic_render { + my $self = shift; + my $renderer = shift; + my $source = shift; + my $output = q{}; + + $renderer->output_string( \$output ); + $renderer->parse_string_document($source); + + return $output; +} + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Pod/XHTML.pm b/lib/MetaCPAN/Pod/XHTML.pm deleted file mode 100644 index 1a01bc7a9..000000000 --- a/lib/MetaCPAN/Pod/XHTML.pm +++ /dev/null @@ -1,43 +0,0 @@ -package MetaCPAN::Pod::XHTML; - -use Moose; -extends 'Pod::Simple::XHTML'; - -sub perldoc_url_prefix { - 'http://metacpan.org/module/' -} - -# thanks to Marc Green - -sub start_item_text { - # see end_item_text -} - -sub end_item_text { - # idify =item content, reset 'scratch' - my $id = $_[0]->idify($_[0]{'scratch'}); - my $text = $_[0]{scratch}; - $_[0]{'scratch'} = ''; - - # construct whole element here because we need the - # contents of the =item to idify it - if ($_[0]{'in_dd'}[ $_[0]{'dl_level'} ]) { - $_[0]{'scratch'} = "\n"; - $_[0]{'in_dd'}[ $_[0]{'dl_level'} ] = 0; - } - - $_[0]{'scratch'} .= qq{
$text
\n
}; - $_[0]{'in_dd'}[ $_[0]{'dl_level'} ] = 1; - $_[0]->emit; -} - -1; - -=pod - -=head2 perldoc_url_prefix - -Set perldoc domain to C. - -=cut - diff --git a/lib/MetaCPAN/Query.pm b/lib/MetaCPAN/Query.pm new file mode 100644 index 000000000..d9160f077 --- /dev/null +++ b/lib/MetaCPAN/Query.pm @@ -0,0 +1,52 @@ +package MetaCPAN::Query; +use Moose; + +use Module::Runtime qw( require_module ); +use Module::Pluggable::Object (); +use MetaCPAN::Types::TypeTiny qw( ES ); + +has es => ( + is => 'ro', + required => 1, + isa => ES, + coerce => 1, +); + +my @plugins = Module::Pluggable::Object->new( + search_path => [__PACKAGE__], + max_depth => 3, + require => 0, +)->plugins; + +for my $class (@plugins) { + require_module($class); + my $name = $class->can('name') && $class->name + or next; + + my $in = "_in_$name"; + my $gen = "_gen_$name"; + + has $in => ( + is => 'ro', + init_arg => $name, + weak_ref => 1, + ); + + has $gen => ( + is => 'ro', + init_arg => undef, + lazy => 1, + default => sub { + my $self = shift; + $class->new( + es => $self->es, + query => $self, + ); + }, + ); + + no strict 'refs'; + *$name = sub { $_[0]->$in // $_[0]->$gen }; +} + +1; diff --git a/lib/MetaCPAN/Query/Author.pm b/lib/MetaCPAN/Query/Author.pm new file mode 100644 index 000000000..f3323e913 --- /dev/null +++ b/lib/MetaCPAN/Query/Author.pm @@ -0,0 +1,132 @@ +package MetaCPAN::Query::Author; + +use MetaCPAN::Moose; + +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Util qw( MAX_RESULT_WINDOW hit_total ); +use Ref::Util qw( is_arrayref ); + +with 'MetaCPAN::Query::Role::Common'; + +sub by_ids { + my ( $self, $ids ) = @_; + + map {uc} @{$ids}; + + my $body = { + query => { ids => { values => $ids } }, + size => scalar @{$ids}, + }; + + my $authors = $self->es->search( es_doc_path('author'), body => $body, ); + + my @authors = map $_->{_source}, @{ $authors->{hits}{hits} }; + + return { + authors => \@authors, + took => $authors->{took}, + total => hit_total($authors), + }; +} + +sub by_user { + my ( $self, $users ) = @_; + $users = [$users] unless is_arrayref($users); + + my $authors = $self->es->search( + es_doc_path('author'), + body => { + query => { terms => { user => $users } }, + size => 500, + } + ); + + my @authors = map $_->{_source}, @{ $authors->{hits}{hits} }; + + return { + authors => \@authors, + took => $authors->{took}, + total => hit_total($authors), + }; +} + +sub search { + my ( $self, $query, $from ) = @_; + + $from //= 0; + my $size = 10; + + if ( $from * $size >= MAX_RESULT_WINDOW ) { + return +{ + authors => [], + took => 0, + total => 0, + }; + } + + my $body = { + query => { + bool => { + should => [ + { + match => { + 'name.analyzed' => + { query => $query, operator => 'AND' } + } + }, + { + match => { + 'asciiname.analyzed' => + { query => $query, operator => 'AND' } + } + }, + { match => { 'pauseid' => uc($query) } }, + { match => { 'profile.id' => lc($query) } }, + ], + } + }, + size => $size, + from => $from || 0, + }; + + my $ret = $self->es->search( es_doc_path('author'), body => $body, ); + + my @authors = map { +{ %{ $_->{_source} }, id => $_->{_id} } } + @{ $ret->{hits}{hits} }; + + return +{ + authors => \@authors, + took => $ret->{took}, + total => hit_total($ret), + }; +} + +sub prefix_search { + my ( $self, $query, $opts ) = @_; + my $size = $opts->{size} // 500; + my $from = $opts->{from} // 0; + + my $body = { + query => { + prefix => { + pauseid => $query, + }, + }, + size => $size, + from => $from, + }; + + my $ret = $self->es->search( es_doc_path('author'), body => $body, ); + + my @authors = map { +{ %{ $_->{_source} }, id => $_->{_id} } } + @{ $ret->{hits}{hits} }; + + return +{ + authors => \@authors, + took => $ret->{took}, + total => hit_total($ret), + }; +} + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Query/CVE.pm b/lib/MetaCPAN/Query/CVE.pm new file mode 100644 index 000000000..20a58d328 --- /dev/null +++ b/lib/MetaCPAN/Query/CVE.pm @@ -0,0 +1,63 @@ +package MetaCPAN::Query::CVE; + +use MetaCPAN::Moose; + +use MetaCPAN::ESConfig qw( es_doc_path ); + +with 'MetaCPAN::Query::Role::Common'; + +sub find_cves_by_cpansa { + my ( $self, $cpansa_id ) = @_; + + my $query = +{ term => { cpansa_id => $cpansa_id } }; + + my $res = $self->es->search( + es_doc_path('cve'), + body => { + query => $query, + size => 999, + } + ); + + return +{ cve => [ map { $_->{_source} } @{ $res->{hits}{hits} } ] }; +} + +sub find_cves_by_release { + my ( $self, $author, $release ) = @_; + + my $query = +{ match => { releases => "$author/$release" } }; + + my $res = $self->es->search( + es_doc_path('cve'), + body => { + query => $query, + size => 999, + } + ); + + return +{ cve => [ map { $_->{_source} } @{ $res->{hits}{hits} } ] }; +} + +sub find_cves_by_dist { + my ( $self, $dist, $version ) = @_; + + my $query = +{ + match => { + dist => $dist, + ( defined $version ? ( versions => $version ) : () ), + } + }; + + my $res = $self->es->search( + es_doc_path('cve'), + body => { + query => $query, + size => 999, + } + ); + + return +{ cve => [ map { $_->{_source} } @{ $res->{hits}{hits} } ] }; +} + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Query/Contributor.pm b/lib/MetaCPAN/Query/Contributor.pm new file mode 100644 index 000000000..d0b1859b9 --- /dev/null +++ b/lib/MetaCPAN/Query/Contributor.pm @@ -0,0 +1,67 @@ +package MetaCPAN::Query::Contributor; + +use MetaCPAN::Moose; + +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Util qw(hit_total); + +with 'MetaCPAN::Query::Role::Common'; + +sub find_release_contributors { + my ( $self, $author, $name ) = @_; + + my $query = +{ + bool => { + must => [ + { term => { release_author => $author } }, + { term => { release_name => $name } }, + { exists => { field => 'pauseid' } }, + ] + } + }; + + my $res = $self->es->search( + es_doc_path('contributor'), + body => { + query => $query, + size => 999, + _source => [ qw( + distribution + pauseid + release_author + release_name + ) ], + } + ); + hit_total($res) or return {}; + + return +{ + contributors => [ map { $_->{_source} } @{ $res->{hits}{hits} } ] }; +} + +sub find_author_contributions { + my ( $self, $pauseid ) = @_; + + my $query = +{ term => { pauseid => $pauseid } }; + + my $res = $self->es->search( + es_doc_path('contributor'), + body => { + query => $query, + size => 999, + _source => [ qw( + distribution + pauseid + release_author + release_name + ) ], + } + ); + hit_total($res) or return {}; + + return +{ + contributors => [ map { $_->{_source} } @{ $res->{hits}{hits} } ] }; +} + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Query/Cover.pm b/lib/MetaCPAN/Query/Cover.pm new file mode 100644 index 000000000..d524ea5c9 --- /dev/null +++ b/lib/MetaCPAN/Query/Cover.pm @@ -0,0 +1,31 @@ +package MetaCPAN::Query::Cover; + +use MetaCPAN::Moose; + +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Util qw(hit_total); + +with 'MetaCPAN::Query::Role::Common'; + +sub find_release_coverage { + my ( $self, $release ) = @_; + + my $query = +{ term => { release => $release } }; + + my $res = $self->es->search( + es_doc_path('cover'), + body => { + query => $query, + size => 999, + } + ); + hit_total($res) or return {}; + + return +{ + %{ $res->{hits}{hits}[0]{_source} }, + url => "http://cpancover.com/latest/$release/index.html", + }; +} + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Query/Distribution.pm b/lib/MetaCPAN/Query/Distribution.pm new file mode 100644 index 000000000..71140d573 --- /dev/null +++ b/lib/MetaCPAN/Query/Distribution.pm @@ -0,0 +1,72 @@ +package MetaCPAN::Query::Distribution; + +use MetaCPAN::Moose; + +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Util qw(hit_total); + +with 'MetaCPAN::Query::Role::Common'; + +sub rogue_list { + return qw( + Acme-DependOnEverything + Bundle-Everything + kurila + perl-5.005_02+apache1.3.3+modperl + perlbench + perl_debug + perl_mlb + pod2texi + spodcxx + ); +} + +sub get_river_data_by_dist { + my ( $self, $dist ) = @_; + + my $query = +{ + bool => { + must => [ { term => { name => $dist } }, ] + } + }; + + my $res = $self->es->search( + es_doc_path('distribution'), + body => { + query => $query, + size => 999, + } + ); + hit_total($res) or return {}; + + return +{ river => +{ $dist => $res->{hits}{hits}[0]{_source}{river} } }; +} + +sub get_river_data_by_dists { + my ( $self, $dist ) = @_; + + my $query = +{ + bool => { + must => [ { terms => { name => $dist } }, ] + } + }; + + my $res = $self->es->search( + es_doc_path('distribution'), + body => { + query => $query, + size => 999, + } + ); + hit_total($res) or return {}; + + return +{ + river => +{ + map { $_->{_source}{name} => $_->{_source}{river} } + @{ $res->{hits}{hits} } + }, + }; +} + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Query/Favorite.pm b/lib/MetaCPAN/Query/Favorite.pm new file mode 100644 index 000000000..67d521657 --- /dev/null +++ b/lib/MetaCPAN/Query/Favorite.pm @@ -0,0 +1,197 @@ +package MetaCPAN::Query::Favorite; + +use MetaCPAN::Moose; + +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Util qw( MAX_RESULT_WINDOW hit_total ); + +with 'MetaCPAN::Query::Role::Common'; + +sub agg_by_distributions { + my ( $self, $distributions, $user ) = @_; + return { + favorites => {}, + myfavorites => {}, + took => 0, + } + unless $distributions; + + my $body = { + size => 0, + query => { + terms => { distribution => $distributions } + }, + aggregations => { + favorites => { + terms => { + field => 'distribution', + size => scalar @{$distributions}, + }, + }, + $user + ? ( + myfavorites => { + filter => { term => { user => $user } }, + aggregations => { + entries => { + terms => { field => 'distribution' } + } + } + } + ) + : (), + } + }; + + my $ret = $self->es->search( es_doc_path('favorite'), body => $body, ); + + my %favorites = map { $_->{key} => $_->{doc_count} } + @{ $ret->{aggregations}{favorites}{buckets} }; + + my %myfavorites; + if ($user) { + %myfavorites = map { $_->{key} => $_->{doc_count} } + @{ $ret->{aggregations}{myfavorites}{entries}{buckets} }; + } + + return { + favorites => \%favorites, + myfavorites => \%myfavorites, + took => $ret->{took}, + }; +} + +sub by_user { + my ( $self, $user, $size ) = @_; + $size ||= 250; + + my $favs = $self->es->search( + es_doc_path('favorite'), + body => { + query => { term => { user => $user } }, + _source => [qw( author date distribution )], + sort => ['distribution'], + size => $size, + } + ); + return {} unless hit_total($favs); + my $took = $favs->{took}; + + my @favs = map { $_->{_source} } @{ $favs->{hits}{hits} }; + + # filter out backpan only distributions + + my $no_backpan = $self->es->search( + es_doc_path('release'), + body => { + query => { + bool => { + must => [ + { terms => { status => [qw( cpan latest )] } }, + { + terms => { + distribution => + [ map { $_->{distribution} } @favs ] + } + }, + ] + } + }, + _source => ['distribution'], + size => scalar(@favs), + } + ); + $took += $no_backpan->{took}; + + if ( hit_total($no_backpan) ) { + my %has_no_backpan = map { $_->{_source}{distribution} => 1 } + @{ $no_backpan->{hits}{hits} }; + + @favs = grep { exists $has_no_backpan{ $_->{distribution} } } @favs; + } + + return { favorites => \@favs, took => $took }; +} + +sub leaderboard { + my $self = shift; + + my $body = { + size => 0, + query => { match_all => {} }, + aggregations => { + leaderboard => { + terms => { + field => 'distribution', + size => 100, + }, + }, + totals => { + cardinality => { + field => 'distribution', + }, + }, + }, + }; + + my $ret = $self->es->search( es_doc_path('favorite'), body => $body, ); + + return { + leaderboard => $ret->{aggregations}{leaderboard}{buckets}, + total => $ret->{aggregations}{totals}{value}, + took => $ret->{took}, + }; +} + +sub recent { + my ( $self, $page, $size ) = @_; + $page //= 1; + $size //= 100; + + if ( $page * $size >= MAX_RESULT_WINDOW ) { + return +{ + favorites => [], + took => 0, + total => 0, + }; + } + + my $favs = $self->es->search( + es_doc_path('favorite'), + body => { + size => $size, + from => ( $page - 1 ) * $size, + query => { match_all => {} }, + sort => [ { 'date' => { order => 'desc' } } ] + } + ); + + my @favs = map { $_->{_source} } @{ $favs->{hits}{hits} }; + + return +{ + favorites => \@favs, + took => $favs->{took}, + total => hit_total($favs), + }; +} + +sub users_by_distribution { + my ( $self, $distribution ) = @_; + + my $favs = $self->es->search( + es_doc_path('favorite'), + body => { + query => { term => { distribution => $distribution } }, + _source => ['user'], + size => 1000, + } + ); + return {} unless hit_total($favs); + + my @plusser_users = map { $_->{_source}{user} } @{ $favs->{hits}{hits} }; + + return { users => \@plusser_users }; +} + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Query/File.pm b/lib/MetaCPAN/Query/File.pm new file mode 100644 index 000000000..15696c491 --- /dev/null +++ b/lib/MetaCPAN/Query/File.pm @@ -0,0 +1,759 @@ +package MetaCPAN::Query::File; + +use MetaCPAN::Moose; + +use List::Util qw( max ); +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Util qw( hit_total true false ); + +with 'MetaCPAN::Query::Role::Common'; + +sub dir { + my ( $self, $author, $release, @path ) = @_; + + my $body = { + query => { + bool => { + must => [ + { term => { 'level' => scalar @path } }, + { term => { 'author' => $author } }, + { term => { 'release' => $release } }, + { + prefix => { + 'path' => join( q{/}, @path, q{} ) + } + }, + ] + }, + }, + size => 999, + _source => [ + qw(name stat.mtime path stat.size directory slop documentation mime) + ], + }; + + my $data = $self->es->search( { + es_doc_path('file'), body => $body, + } ); + + my $dir = [ map { $_->{_source} } @{ $data->{hits}{hits} } ]; + + return { dir => $dir }; +} + +sub _doc_files { + my @files = @_; + my %s; + return + map +( "$_", "$_.pod", "$_.md", "$_.markdown", "$_.mdown", + "$_.mkdn", ), + grep !$s{$_}++, + map +( $_, uc $_ ), + @_; +} + +my %special_files = ( + changelog => [ + _doc_files( qw( + Changelog + ChangeLog + Changes + News + ) ), + ], + contributing => [ + _doc_files( qw( + Contributing + Hacking + Development + ) ), + ], + license => [ qw( + LICENCE + LICENSE + Copyright + COPYRIGHT + Copying + COPYING + Artistic + ARTISTIC + ) ], + install => [ + _doc_files( qw( + Install + ) ), + ], + dist => [ qw( + Build.PL + MANIFEST + META.json + META.yml + Makefile.PL + alienfile + cpanfile + prereqs.json + prereqs.yml + dist.ini + minil.toml + ) ], + security => [ + _doc_files( qw( + Security + security + ) ), + qw( + security.txt + ), + ], + other => [ + _doc_files( qw( + Authors + Credits + FAQ + README + THANKS + ToDo + Todo + ) ), + ], +); +my %perl_files = ( + changelog => [ qw( + perldelta.pod + ) ], + license => [ qw( + perlartistic.pod + perlgpl.pod + ) ], + contributing => [ qw( + perlhack.pod + ) ], +); + +my @shared_path_prefix_examples = qw( + example + examples + Example + Examples + sample + samples + demo + demos +); + +my %path_files = ( + example => [ + qw( + eg + ex + ), + @shared_path_prefix_examples, + ], +); + +my %prefix_files = ( example => [ @shared_path_prefix_examples, ], ); + +my %file_to_type; +my %type_to_regex; +my %query_parts; + +my %sort_order; + +for my $type ( keys %special_files ) { + my @files = @{ $special_files{$type} || [] }; + my @perl_files = @{ $perl_files{$type} || [] }; + + $sort_order{ $files[$_] } = $_ for 0 .. $#files; + + my @root_file = grep !/\.pod$/, @files; + my @non_root_file = grep /\.pod$/, @files; + + my @parts; + if (@root_file) { + push @parts, + { + bool => { + must => [ + { term => { level => 0 } }, + { terms => { name => \@root_file } }, + ], + ( + @perl_files + ? ( must_not => + [ { term => { distribution => 'perl' } } ] ) + : () + ), + } + }; + } + if (@non_root_file) { + push @parts, + { + bool => { + must => [ { terms => { name => \@non_root_file } } ], + ( + @perl_files + ? ( must_not => + [ { term => { distribution => 'perl' } } ] ) + : () + ), + } + }; + } + if (@perl_files) { + push @parts, + { + bool => { + must => [ + { term => { distribution => 'perl' } }, + { terms => { name => \@perl_files } }, + ], + } + }; + } + + $file_to_type{$_} = $type for @files, @perl_files; + push @{ $query_parts{$type} }, @parts; +} + +for my $type ( keys %prefix_files ) { + my @prefixes = @{ $prefix_files{$type} }; + + my @parts = map +{ prefix => { 'name' => $_ } }, @prefixes; + + push @{ $query_parts{$type} }, @parts; + + my ($regex) = map qr{(?:\A|/)(?:$_)[^/]*\z}, join '|', @prefixes; + + if ( $type_to_regex{$type} ) { + $type_to_regex{$type} = qr{$type_to_regex{$type}|$regex}; + } + else { + $type_to_regex{$type} = $regex; + } +} + +for my $type ( keys %path_files ) { + my @prefixes = @{ $path_files{$type} }; + + my @parts = map +{ prefix => { 'path' => "$_/" } }, @prefixes; + + push @{ $query_parts{$type} }, @parts; + + my ($regex) = map qr{\A(?:$_)/}, join '|', @prefixes; + + if ( $type_to_regex{$type} ) { + $type_to_regex{$type} = qr{$type_to_regex{$type}|$regex}; + } + else { + $type_to_regex{$type} = $regex; + } +} + +sub interesting_files { + my ( $self, $author, $release, $categories, $options ) = @_; + + $categories = [ sort keys %query_parts ] + if !$categories || !@$categories; + + my $return = { + files => [], + total => 0, + took => 0, + }; + + my @clauses = map @{ $query_parts{$_} // [] }, @$categories; + + return $return + unless @clauses; + + $options->{_source} //= [ qw( + author + distribution + documentation + name + path + pod_lines + release + status + ) ]; + $options->{size} //= 250; + + my $body = { + query => { + bool => { + must => [ + { term => { release => $release } }, + { term => { author => $author } }, + { term => { directory => false } }, + { bool => { should => \@clauses } }, + ], + must_not => [ + { prefix => { 'path' => 'corpus/' } }, + { prefix => { 'path' => 'fatlib/' } }, + { prefix => { 'path' => 'inc/' } }, + { prefix => { 'path' => 'local/' } }, + { prefix => { 'path' => 'perl5/' } }, + { prefix => { 'path' => 'share/' } }, + { prefix => { 'path' => 't/' } }, + { prefix => { 'path' => 'xt/' } }, + ], + }, + }, + %$options, + }; + + my $data = $self->es->search( { + es_doc_path('file'), body => $body, + } ); + + $return->{took} = $data->{took}; + $return->{total} = hit_total($data); + + return $return + unless $return->{total}; + + my $files = [ map $_->{_source}, @{ $data->{hits}{hits} } ]; + + for my $file (@$files) { + my $category = $file_to_type{ $file->{name} }; + if ( !$category ) { + for my $type ( keys %type_to_regex ) { + if ( $file->{path} =~ $type_to_regex{$type} ) { + $category = $type; + last; + } + } + } + $category ||= 'unknown'; + + $file->{category} = $category; + } + + $return->{files} = $files; + + return $return; +} + +sub files_by_category { + my ( $self, $author, $release, $categories, $options ) = @_; + my $return = $self->interesting_files( $author, $release, $categories, + $options ); + my $files = delete $return->{files}; + + $return->{categories} = { map +( $_ => [] ), @$categories }; + + for my $file (@$files) { + my $category = $file->{category}; + push @{ $return->{categories}{$category} }, $file; + } + + for my $category (@$categories) { + my $files = $return->{categories}{$category}; + @$files = map $_->[0], + sort { $a->[1] <=> $b->[1] || $a->[2] cmp $b->[2] } + map [ $_, $sort_order{ $_->{name} } || 9999, $_->{path} ], + @$files; + } + return $return; +} + +sub find_changes_files { + my ( $self, $author, $release ) = @_; + my $result = $self->files_by_category( $author, $release, ['changelog'], + { _source => true } ); + my ($file) = @{ $result->{categories}{changelog} || [] }; + return $file; +} + +sub _autocomplete { + my ( $self, $query ) = @_; + + my $search_size = 100; + + my $sugg_res = $self->es->search( + es_doc_path('file'), + body => { + suggest => { + documentation => { + text => $query, + completion => { + field => "suggest", + size => $search_size, + }, + }, + } + }, + ); + + my %docs; + for my $suggest ( @{ $sugg_res->{suggest}{documentation}[0]{options} } ) { + $docs{ $suggest->{text} } = max grep {defined} + ( $docs{ $suggest->{text} }, $suggest->{score} ); + } + + my $res = $self->es->search( + es_doc_path('file'), + body => { + query => { + bool => { + must => [ + { term => { indexed => true } }, + { term => { authorized => true } }, + { term => { status => 'latest' } }, + { terms => { documentation => [ keys %docs ] } }, + ], + must_not => [ + { + terms => { + distribution => [ + $self->query->distribution->rogue_list + ] + }, + }, + ], + } + }, + _source => [ qw( + author + date + deprecated + distribution + documentation + release + ) ], + size => $search_size, + }, + ); + + my $hits = $res->{hits}{hits}; + + my $fav_res + = $self->query->favorite->agg_by_distributions( + [ map $_->{_source}{distribution}, @$hits ] ); + + my $favs = $fav_res->{favorites}; + + my %valid = map { + my $source = $_->{_source}; + ( + $source->{documentation} => { + %$source, favorites => $favs->{ $source->{distribution} }, + } + ); + } @{ $res->{hits}{hits} }; + + # remove any exact match, it will be added later + my $exact = delete $valid{$query}; + + no warnings 'uninitialized'; + my @sorted = map { $valid{$_} } + sort { + my $a_data = $valid{$a}; + my $b_data = $valid{$b}; + $a_data->{deprecated} <=> $b_data->{deprecated} + || $b_data->{favorites} <=> $a_data->{favorites} + || $docs{$b} <=> $docs{$a} + || length($a) <=> length($b) + || $a cmp $b + } + keys %valid; + + return { + took => $sugg_res->{took} + $res->{took} + $fav_res->{took}, + suggestions => [ ( $exact ? $exact : () ), @sorted ], + }; +} + +sub autocomplete { + my ( $self, @terms ) = @_; + my $data = $self->_autocomplete( join ' ', @terms ); + + return { + took => $data->{took}, + hits => { + hits => [ + map { + my $source = $_; + +{ + fields => { + map +( $_ => $source->{$_} ), qw( + documentation + release + author + distribution + ), + }, + }; + } @{ $data->{suggestions} } + ], + }, + }; +} + +sub autocomplete_suggester { + my ( $self, @terms ) = @_; + my $data = $self->_autocomplete( join ' ', @terms ); + + return { + took => $data->{took}, + suggestions => [ + map +{ + author => $_->{author}, + date => $_->{date}, + deprecated => $_->{deprecated}, + distribution => $_->{distribution}, + name => $_->{documentation}, + release => $_->{release}, + }, + @{ $data->{suggestions} } + ], + }; +} + +sub documented_modules { + my ( $self, $author, $release ) = @_; + my $query = { + bool => { + must => [ + { term => { author => $author } }, + { term => { release => $release } }, + { exists => { field => "documentation" } }, + { + bool => { + should => [ + { + bool => { + must => [ + { + exists => + { field => 'module.name' } + }, + { + term => + { 'module.indexed' => true } + }, + ], + } + }, + { + bool => { + must => [ + { + exists => + { field => 'pod.analyzed' } + }, + { term => { indexed => true } }, + ], + } + }, + ], + } + }, + ], + }, + }; + my $res = $self->es->search( + es_doc_path('file'), + body => { + query => $query, + size => 999, + _source => [qw(name module path documentation distribution)], + }, + ); + + return { + took => $res->{took}, + files => [ map $_->{_source}, @{ $res->{hits}{hits} } ], + }; +} + +sub find_module { + my ( $self, $module, $fields ) = @_; + + my $query = { + bool => { + must => [ + { term => { indexed => true } }, + { term => { authorized => true } }, + { term => { status => 'latest' } }, + { + bool => { + should => [ + { term => { documentation => $module } }, + { + nested => { + path => "module", + query => { + bool => { + must => [ + { + term => { "module.name" => + $module } + }, + { + bool => { should => + [ + { term => + { "module.authorized" + => true + } }, + { exists => + { field => + 'module.associated_pod' + } }, + ], + } + }, + ], + }, + }, + } + }, + ] + } + }, + ], + }, + }; + + my $res = $self->es->search( + es_doc_path('file'), + search_type => 'dfs_query_then_fetch', + body => { + query => $query, + sort => [ + '_score', + { 'version_numified' => { order => 'desc' } }, + { 'date' => { order => 'desc' } }, + { 'mime' => { order => 'asc' } }, + { 'stat.mtime' => { order => 'desc' } } + ], + _source => [ + qw( documentation module.indexed module.authoried module.name ) + ], + size => 100, + }, + ); + + my @candidates = @{ $res->{hits}{hits} }; + + my ($file) = grep { + grep { $_->{indexed} && $_->{authorized} && $_->{name} eq $module } + @{ $_->{module} || [] } + } grep { !$_->{documentation} || $_->{documentation} eq $module } + @candidates; + + $file ||= shift @candidates; + return undef + if !$file; + return $self->es->get_source( + es_doc_path('file'), + id => $file->{_id}, + ( $fields ? ( _source => $fields ) : () ), + ); +} + +sub find_pod { + my ( $self, $name ) = @_; + my $file = $self->find_module($name); + return $file + unless $file; + my ($module) + = grep { $_->{indexed} && $_->{authorized} && $_->{name} eq $name } + @{ $file->{module} || [] }; + if ( $module && ( my $pod = $module->{associated_pod} ) ) { + my ( $author, $release, @path ) = split( /\//, $pod ); + my $query = { + bool => { + must => [ + { term => { author => $author } }, + { term => { release => $release } }, + { term => { path => join( '/', @path ) } }, + ], + }, + }; + my $pod_file = $self->es->search( + es_doc_path('file'), + body => { + query => $query, + }, + ); + return $pod_file->{hits}{hits}[0]->{_source}; + } + else { + return $file; + } +} + +sub history { + my ( $self, $type, $name, $path, $opts ) = @_; + + $opts ||= {}; + if ( ref $path ) { + $path = join '/', @$path; + } + + my $source = $opts->{fields}; + + my $query + = $type eq "module" + ? { + nested => { + path => 'module', + query => { + constant_score => { + filter => { + bool => { + must => [ + { term => { "module.authorized" => true } }, + { term => { "module.indexed" => true } }, + { term => { "module.name" => $name } }, + ] + } + } + } + } + } + } + : $type eq "file" ? { + bool => { + must => [ + { term => { path => $path } }, + { term => { distribution => $name } }, + ] + } + } + + # XXX: to fix: no filtering on 'release' so this query + # will produce modules matching duplications. -- Mickey + : $type eq "documentation" ? { + bool => { + must => [ + { match_phrase => { documentation => $name } }, + { term => { indexed => true } }, + { term => { authorized => true } }, + ] + } + } + : return undef; + + my $res = $self->es->search( + es_doc_path('file'), + body => { + query => $query, + size => 500, + sort => [ { date => 'desc' } ], + ( $source ? ( _source => $source ) : () ), + }, + ); + + return { + took => $res->{took}, + total => hit_total($res), + files => [ map $_->{_source}, @{ $res->{hits}{hits} } ], + }; +} + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Query/Mirror.pm b/lib/MetaCPAN/Query/Mirror.pm new file mode 100644 index 000000000..c16ee36e0 --- /dev/null +++ b/lib/MetaCPAN/Query/Mirror.pm @@ -0,0 +1,68 @@ +package MetaCPAN::Query::Mirror; + +use MetaCPAN::Moose; +use MetaCPAN::Util qw( hit_total ); + +use MetaCPAN::ESConfig qw( es_doc_path ); + +with 'MetaCPAN::Query::Role::Common'; + +sub search { + my ( $self, $q ) = @_; + my $query = { match_all => {} }; + + if ($q) { + my @protocols = grep /^ (?: http | ftp | rsync ) $/x, split /\s+/, $q; + + $query = { + bool => { + must => [ map +{ exists => { field => $_ } }, @protocols ] + }, + }; + } + + my @sort = ( sort => [qw( continent country )] ); + + my $location; + + if ( $q and $q =~ /loc\:([^\s]+)/ ) { + $location = [ split( /,/, $1 ) ]; + if ($location) { + @sort = ( + sort => { + _geo_distance => { + location => [ $location->[1], $location->[0] ], + order => 'asc', + unit => 'km' + } + } + ); + } + } + + my $ret = $self->es->search( + es_doc_path('mirror'), + body => { + size => 999, + query => $query, + @sort, + }, + ); + + my $data = [ + map +{ + %{ $_->{_source} }, + distance => ( $location ? $_->{sort}[0] : undef ) + }, + @{ $ret->{hits}{hits} } + ]; + + return { + mirrors => $data, + total => hit_total($ret), + took => $ret->{took} + }; +} + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Query/Package.pm b/lib/MetaCPAN/Query/Package.pm new file mode 100644 index 000000000..00027f683 --- /dev/null +++ b/lib/MetaCPAN/Query/Package.pm @@ -0,0 +1,35 @@ +package MetaCPAN::Query::Package; + +use MetaCPAN::Moose; + +use MetaCPAN::ESConfig qw( es_doc_path ); + +with 'MetaCPAN::Query::Role::Common'; + +sub get_modules { + my ( $self, $dist, $ver ) = @_; + + my $query = +{ + bool => { + must => [ + { term => { distribution => $dist } }, + { term => { dist_version => $ver } }, + ], + } + }; + + my $res = $self->es->search( + es_doc_path('package'), + body => { + query => $query, + size => 999, + _source => [qw< module_name >], + } + ); + + return +{ modules => + [ map { $_->{_source}{module_name} } @{ $res->{hits}{hits} } ] }; +} + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Query/Permission.pm b/lib/MetaCPAN/Query/Permission.pm new file mode 100644 index 000000000..311a68aab --- /dev/null +++ b/lib/MetaCPAN/Query/Permission.pm @@ -0,0 +1,62 @@ +package MetaCPAN::Query::Permission; + +use MetaCPAN::Moose; + +use MetaCPAN::ESConfig qw( es_doc_path ); +use Ref::Util qw( is_arrayref ); + +with 'MetaCPAN::Query::Role::Common'; + +sub by_author { + my ( $self, $pauseid ) = @_; + + my $body = { + query => { + bool => { + should => [ + { term => { owner => $pauseid } }, + { term => { co_maintainers => $pauseid } }, + ], + }, + }, + size => 5_000, + }; + + my $ret = $self->es->search( es_doc_path('permission'), body => $body, ); + + my $data = [ + sort { $a->{module_name} cmp $b->{module_name} } + map { $_->{_source} } @{ $ret->{hits}{hits} } + ]; + + return { permissions => $data }; +} + +sub by_modules { + my ( $self, $modules ) = @_; + $modules = [$modules] unless is_arrayref($modules); + + my @modules = map +{ term => { module_name => $_ } }, + grep defined, @{$modules}; + return { permissions => [] } + unless @modules; + + my $body = { + query => { + bool => { should => \@modules } + }, + size => 1_000, + }; + + my $ret = $self->es->search( es_doc_path('permission'), body => $body, ); + + my $data = [ + sort { $a->{module_name} cmp $b->{module_name} } + map { $_->{_source} } @{ $ret->{hits}{hits} } + ]; + + return { permissions => $data }; +} + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Query/Release.pm b/lib/MetaCPAN/Query/Release.pm new file mode 100644 index 000000000..b45011e25 --- /dev/null +++ b/lib/MetaCPAN/Query/Release.pm @@ -0,0 +1,1142 @@ +package MetaCPAN::Query::Release; +use v5.20; + +use MetaCPAN::Moose; + +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Util + qw( MAX_RESULT_WINDOW hit_total single_valued_arrayref_to_scalar true false ); + +with 'MetaCPAN::Query::Role::Common'; + +sub author_status { + my ( $self, $id, $file ) = @_; + return unless $id and $file; + + my $status = $file->{_source} + || single_valued_arrayref_to_scalar( $file->{fields} ); + + if ( $status and $status->{pauseid} ) { + $status->{release_count} + = $self->aggregate_status_by_author( $status->{pauseid} ); + + my ( $id_2, $id_1 ) = $id =~ /^((\w)\w)/; + $status->{links} = { + cpan_directory => + "https://www.cpan.org/authors/id/$id_1/$id_2/$id", + backpan_directory => + "https://cpan.metacpan.org/authors/id/$id_1/$id_2/$id", + cpants => "https://cpants.cpanauthors.org/author/$id", + cpantesters_reports => + "https://www.cpantesters.org/author/$id_1/$id.html", + cpantesters_matrix => + "https://matrix.cpantesters.org/?author=$id", + metacpan_explorer => + "https://explorer.metacpan.org/?url=/author/$id", + repology => "https://repology.org/maintainer/$id%40cpan", + }; + } + + return $status; +} + +sub aggregate_status_by_author { + my ( $self, $pauseid ) = @_; + my $agg = $self->es->search( { + es_doc_path('release'), + body => { + query => { + term => { author => $pauseid } + }, + aggregations => { + count => { terms => { field => 'status' } } + }, + size => 0, + } + } ); + my %ret = ( cpan => 0, latest => 0, backpan => 0 ); + if ($agg) { + $ret{ $_->{'key'} } = $_->{'doc_count'} + for @{ $agg->{'aggregations'}{'count'}{'buckets'} }; + } + $ret{'backpan-only'} = delete $ret{'backpan'}; + return \%ret; +} + +sub get_contributors { + my ( $self, $author_name, $release_name ) = @_; + + my $res = $self->es->search( + es_doc_path('contributor'), + body => { + query => { + bool => { + must => [ + { term => { release_name => $release_name } }, + { term => { release_author => $author_name } }, + ], + }, + }, + size => 999, + _source => [qw< email name pauseid >], + } + ); + + my @contribs = map $_->{_source}, @{ $res->{hits}{hits} }; + + @contribs = sort { fc $a->{name} cmp fc $b->{name} } @contribs; + + return { contributors => \@contribs }; +} + +sub get_files { + my ( $self, $release, $files ) = @_; + + my $query = +{ + query => { + bool => { + must => [ + { term => { release => $release } }, + { terms => { name => $files } } + ], + } + } + }; + + my $ret = $self->es->search( + es_doc_path('file'), + body => { + query => $query, + size => 999, + _source => [qw< name path >], + } + ); + + return {} unless @{ $ret->{hits}{hits} }; + + return { files => [ map { $_->{_source} } @{ $ret->{hits}{hits} } ] }; +} + +sub get_checksums { + my ( $self, $release ) = @_; + + my $query = { term => { name => $release } }; + + my $ret = $self->es->search( + es_doc_path('release'), + body => { + query => $query, + size => 1, + _source => [qw< checksum_md5 checksum_sha256 >], + } + ); + + return {} unless @{ $ret->{hits}{hits} }; + return $ret->{hits}{hits}[0]{_source}; +} + +sub _activity_filters { + my ( $self, $params, $start ) = @_; + my ( $author, $distribution, $module, $new_dists ) + = @{$params}{qw( author distribution module new_dists )}; + + my @filters + = ( { range => { date => { from => $start->epoch . '000' } } } ); + + push @filters, +{ term => { author => uc($author) } } + if $author; + + push @filters, +{ term => { distribution => $distribution } } + if $distribution; + + push @filters, +{ term => { 'dependency.module' => $module } } + if $module; + + if ( $new_dists and $new_dists eq 'n' ) { + push @filters, + ( + +{ term => { first => true } }, + +{ terms => { status => [qw( cpan latest )] } }, + ); + } + + return +{ bool => { must => \@filters } }; +} + +sub activity { + my ( $self, $params ) = @_; + my $res = $params->{res} // '1w'; + + my $start + = DateTime->now->truncate( to => 'month' )->subtract( months => 23 ); + + my $filters = $self->_activity_filters( $params, $start ); + + my $body = { + query => { match_all => {} }, + aggregations => { + histo => { + filter => $filters, + aggregations => { + entries => { + date_histogram => + { field => 'date', interval => $res }, + } + } + } + }, + size => 0, + }; + + my $ret = $self->es->search( es_doc_path('release'), body => $body, ); + + my $data = { map { $_->{key} => $_->{doc_count} } + @{ $ret->{aggregations}{histo}{entries}{buckets} } }; + + my $line = [ + map { + $data->{ $start->clone->add( months => $_ )->epoch . '000' } + || 0 + } ( 0 .. 23 ) + ]; + + return { activity => $line }; +} + +sub by_author_and_name { + my ( $self, $author, $release ) = @_; + + my $body = { + query => { + bool => { + must => [ + { term => { 'name' => $release } }, + { term => { author => uc($author) } } + ] + } + } + }; + + my $ret = $self->es->search( es_doc_path('release'), body => $body, ); + + my $data = $ret->{hits}{hits}[0]{_source}; + + return { + took => $ret->{took}, + release => $data, + total => hit_total($ret), + }; +} + +sub by_author_and_names { + my ( $self, $releases ) = @_; + + # $releases: ArrayRef[ Dict[ author => Str, name => Str ] ] + + my $body = { + size => ( 0 + @$releases ), + query => { + bool => { + should => [ + map { + +{ + bool => { + must => [ + { + term => { + author => uc( $_->{author} ) + } + }, + { term => { 'name' => $_->{name} } }, + ] + } + } + } @$releases + ] + } + } + }; + + my $ret = $self->es->search( es_doc_path('release'), body => $body, ); + + my @releases; + for my $hit ( @{ $ret->{hits}{hits} } ) { + my $src = $hit->{_source}; + push @releases, $src; + } + + return { + took => $ret->{took}, + total => hit_total($ret), + releases => \@releases, + }; +} + +sub by_author { + my ( $self, $pauseid, $page, $size ) = @_; + $size //= 1000; + $page //= 1; + + if ( $page * $size >= MAX_RESULT_WINDOW ) { + return { + releases => [], + took => 0, + total => 0, + }; + } + + my $body = { + query => { + bool => { + must => [ + { terms => { status => [qw< cpan latest >] } }, + ( $pauseid ? { term => { author => $pauseid } } : () ), + ], + } + }, + sort => + [ 'distribution', { 'version_numified' => { reverse => 1 } } ], + _source => [ + qw( abstract author authorized date distribution license metadata.version resources.repository status tests ) + ], + size => $size, + from => ( $page - 1 ) * $size, + }; + + my $ret = $self->es->search( es_doc_path('release'), body => $body, ); + + my $data = [ map { $_->{_source} } @{ $ret->{hits}{hits} } ]; + + return { + releases => $data, + total => hit_total($ret), + took => $ret->{took} + }; +} + +sub latest_by_distribution { + my ( $self, $distribution ) = @_; + + my $body = { + query => { + bool => { + must => [ + { + term => { + 'distribution' => $distribution + } + }, + { term => { status => 'latest' } } + ] + } + }, + sort => [ { date => 'desc' } ], + size => 1 + }; + + my $ret = $self->es->search( es_doc_path('release'), body => $body, ); + + my $data = $ret->{hits}{hits}[0]{_source}; + + return { + release => $data, + took => $ret->{took}, + total => hit_total($ret), + }; +} + +sub latest_by_author { + my ( $self, $pauseid ) = @_; + + my $body = { + query => { + bool => { + must => [ + { term => { author => uc($pauseid) } }, + { term => { status => 'latest' } } + ] + } + }, + sort => + [ 'distribution', { 'version_numified' => { reverse => 1 } } ], + _source => [ + qw(author distribution name status abstract date download_url version authorized maturity) + ], + size => 1000, + }; + + my $ret = $self->es->search( es_doc_path('release'), body => $body, ); + + my $data = [ map { $_->{_source} } @{ $ret->{hits}{hits} } ]; + + return { took => $ret->{took}, releases => $data }; +} + +sub all_by_author { + my ( $self, $author, $page, $size ) = @_; + $size //= 100; + $page //= 1; + + if ( $page * $size >= MAX_RESULT_WINDOW ) { + return { + releases => [], + took => 0, + total => 0, + }; + } + + my $body = { + query => { term => { author => uc($author) } }, + sort => [ { date => 'desc' } ], + _source => [ + qw(author distribution name status abstract date download_url version authorized maturity) + ], + size => $size, + from => ( $page - 1 ) * $size, + }; + my $ret = $self->es->search( es_doc_path('release'), body => $body, ); + + my $data = [ map { $_->{_source} } @{ $ret->{hits}{hits} } ]; + + return { + took => $ret->{took}, + releases => $data, + total => hit_total($ret), + }; +} + +sub versions { + my ( $self, $dist, $versions ) = @_; + + my $size = 1000; + + my $query; + + # 'versions' param was sent + if ( @{$versions} ) { + my $filter_versions; + + # we only want 'latest' version + if ( @{$versions} == 1 and $versions->[0] eq 'latest' ) { + $filter_versions = { term => { status => 'latest' } }; + } + else { + if ( grep $_ eq 'latest', @{$versions} ) { + + # we want a combination of 'latest' and specific versions + @{$versions} = grep $_ ne 'latest', @{$versions}; + $filter_versions = { + bool => { + should => [ + { terms => { version => $versions } }, + { term => { status => 'latest' } }, + ], + } + }; + } + else { + # we only want specific versions + $filter_versions = { terms => { version => $versions } }; + } + } + + $query = { + bool => { + must => [ + { term => { distribution => $dist } }, + $filter_versions + ] + } + }; + } + else { + $query = { term => { distribution => $dist } }; + } + + my $body = { + query => $query, + size => $size, + sort => [ { date => 'desc' } ], + _source => [ qw( + name + date + author + version + status + maturity + authorized + download_url + main_module + ) ], + }; + + my $ret = $self->es->search( es_doc_path('release'), body => $body, ); + + my $data = [ map { $_->{_source} } @{ $ret->{hits}{hits} } ]; + + return { + releases => $data, + total => hit_total($ret), + took => $ret->{took} + }; +} + +sub top_uploaders { + my ( $self, $range ) = @_; + my $range_filter = { + range => { + date => { + from => $range eq 'all' ? 0 : DateTime->now->subtract( + $range eq 'weekly' ? 'weeks' + : $range eq 'monthly' ? 'months' + : $range eq 'yearly' ? 'years' + : 'weeks' => 1 + )->truncate( to => 'day' )->iso8601 + }, + } + }; + + my $body = { + query => { match_all => {} }, + aggregations => { + author => { + aggregations => { + entries => { + terms => { field => 'author', size => 50 } + } + }, + filter => $range_filter, + }, + }, + size => 0, + }; + + my $ret = $self->es->search( es_doc_path('release'), body => $body, ); + + my $counts = { map { $_->{key} => $_->{doc_count} } + @{ $ret->{aggregations}{author}{entries}{buckets} } }; + + return { + counts => $counts, + took => $ret->{took} + }; +} + +sub requires { + my ( $self, $module, $page, $page_size, $sort ) = @_; + return $self->_get_depended_releases( [$module], $page, $page_size, + $sort ); +} + +sub reverse_dependencies { + my ( $self, $distribution, $page, $page_size, $sort ) = @_; + + # get the latest release of given distribution + my $release = $self->_get_latest_release($distribution) || return; + + # get (authorized/indexed) modules provided by the release + my $modules = $self->_get_provided_modules($release) || return; + + # return releases depended on those modules + return $self->_get_depended_releases( $modules, $page, $page_size, + $sort ); +} + +sub _get_latest_release { + my ( $self, $distribution ) = @_; + + my $release = $self->es->search( + es_doc_path('release'), + body => { + query => { + bool => { + must => [ + { term => { distribution => $distribution } }, + { term => { status => 'latest' } }, + { term => { authorized => true } }, + ] + }, + }, + _source => [qw< name author >], + }, + ); + + my ($release_info) = map { $_->{_source} } @{ $release->{hits}{hits} }; + + return $release_info->{name} && $release_info->{author} + ? +{ + name => $release_info->{name}, + author => $release_info->{author}, + } + : undef; +} + +sub _get_provided_modules { + my ( $self, $release ) = @_; + + my $provided_modules = $self->es->search( + es_doc_path('file'), + body => { + query => { + bool => { + must => [ + { term => { 'release' => $release->{name} } }, + { term => { 'author' => $release->{author} } }, + { term => { 'module.authorized' => true } }, + { term => { 'module.indexed' => true } }, + ] + } + }, + size => 999, + } + ); + + my @modules = map { $_->{name} } + grep { $_->{indexed} && $_->{authorized} } + map { @{ $_->{_source}{module} } } + @{ $provided_modules->{hits}{hits} }; + + return @modules ? \@modules : undef; +} + +sub _fix_sort_value { + my $sort = shift; + + if ( $sort && $sort =~ /^(\w+):(asc|desc)$/ ) { + return { $1 => $2 }; + } + else { + return { date => 'desc' }; + } +} + +sub _get_depended_releases { + my ( $self, $modules, $page, $page_size, $sort ) = @_; + $page //= 1; + $page_size //= 50; + + if ( $page * $page_size >= MAX_RESULT_WINDOW ) { + return +{ + data => [], + took => 0, + total => 0, + }; + } + + $sort = _fix_sort_value($sort); + + my $dependency_filter = { + nested => { + path => 'dependency', + query => { + bool => { + must => [ + { + term => + { 'dependency.relationship' => 'requires' } + }, + { + terms => { + 'dependency.phase' => [ qw( + configure + build + runtime + test + ) ] + } + }, + { terms => { 'dependency.module' => $modules } }, + ], + }, + }, + }, + }; + + my $depended = $self->es->search( + es_doc_path('release'), + body => { + query => { + bool => { + must => [ + $dependency_filter, + { term => { status => 'latest' } }, + { term => { authorized => true } }, + ], + }, + }, + size => $page_size, + from => ( $page - 1 ) * $page_size, + sort => $sort, + } + ); + + return +{ + data => [ map { $_->{_source} } @{ $depended->{hits}{hits} } ], + total => hit_total($depended), + took => $depended->{took}, + }; +} + +sub recent { + my ( $self, $type, $page, $page_size ) = @_; + $page //= 1; + $page_size //= 10000; + $type //= ''; + + if ( $page * $page_size >= MAX_RESULT_WINDOW ) { + return +{ + releases => [], + took => 0, + total => 0, + }; + } + + my $query; + if ( $type eq 'n' ) { + $query = { + bool => { + must => [ + { term => { first => true } }, + { terms => { status => [qw< cpan latest >] } }, + ] + } + }; + } + elsif ( $type eq 'a' ) { + $query = { match_all => {} }; + } + else { + $query = { terms => { status => [qw< cpan latest >] } }; + } + + my $body = { + size => $page_size, + from => ( $page - 1 ) * $page_size, + query => $query, + _source => + [qw(name author status abstract date distribution maturity)], + sort => [ { 'date' => { order => 'desc' } } ] + }; + + my $ret = $self->es->search( es_doc_path('release'), body => $body, ); + + my $data = [ map { $_->{_source} } @{ $ret->{hits}{hits} } ]; + + return { + releases => $data, + total => hit_total($ret), + took => $ret->{took} + }; +} + +sub modules { + my ( $self, $author, $release ) = @_; + + my $body = { + query => { + bool => { + must => [ + { term => { release => $release } }, + { term => { author => $author } }, + { term => { directory => false } }, + { + bool => { + should => [ + { + bool => { + must => [ + { + exists => { + field => 'module.name' + } + }, + { + term => { + 'module.indexed' => true + } + } + ] + } + }, + { + bool => { + must => [ + { + range => { + slop => { gt => 0 } + } + }, + { + exists => { + field => 'pod.analyzed' + } + }, + { + term => { 'indexed' => true } + }, + ] + } + } + ] + } + } + ] + } + }, + size => 999, + + # Sort by documentation name; if there isn't one, sort by path. + sort => [ 'documentation', 'path' ], + + _source => [ qw( + module + abstract + author + authorized + distribution + documentation + indexed + path + pod_lines + release + status + ) ], + }; + + my $ret = $self->es->search( es_doc_path('file'), body => $body, ); + + my @files = map $_->{_source}, @{ $ret->{hits}{hits} }; + + return { + files => \@files, + total => hit_total($ret), + took => $ret->{took} + }; +} + +=head2 find_download_url + +cpanm Foo +=> status: latest, maturity: released + +cpanm --dev Foo +=> status: -backpan, sort_by: version_numified,date + +cpanm Foo~1.0 +=> status: latest, maturity: released, module.version_numified: gte: 1.0 + +cpanm --dev Foo~1.0 +-> status: -backpan, module.version_numified: gte: 1.0, sort_by: version_numified,date + +cpanm Foo~<2 +=> maturity: released, module.version_numified: lt: 2, sort_by: status,version_numified,date + +cpanm --dev Foo~<2 +=> status: -backpan, module.version_numified: lt: 2, sort_by: status,version_numified,date + + $release->find_download_url( 'module', 'Foo', { version => $version, dev => 0|1 }); + +Sorting: + + if it's stable: + prefer latest > cpan > backpan + then sort by version desc + then sort by date descending (rev chron) + + if it's dev: + sort by version desc + sort by date descending (reverse chronologically) + + +=cut + +sub find_download_url { + my ( $self, $type, $name, $args ) = @_; + $args ||= {}; + + my $dev = $args->{dev}; + my $version = $args->{version}; + my $explicit_version = $version && $version =~ /==/; + + my @filters; + + die 'type must be module or dist' + unless $type eq 'module' || $type eq 'dist'; + my $module_filter = $type eq 'module'; + + if ( !$explicit_version ) { + push @filters, + { bool => { must_not => [ { term => { status => 'backpan' } } ] } + }; + if ( !$dev ) { + push @filters, { term => { maturity => 'released' } }; + } + } + + my $prefix = $module_filter ? 'module.' : ''; + + my $version_filters + = $self->_version_filters( $version, $prefix . 'version_numified' ); + + my $entity_filter = { + bool => { + must => [ + { term => { $prefix . 'authorized' => true } }, + ( + $module_filter + ? ( + { term => { $prefix . 'indexed' => true } }, + { term => { $prefix . 'name' => $name } } + ) + : { term => { 'distribution' => $name } }, + ), + ( + exists $version_filters->{must} + ? @{ $version_filters->{must} } + : () + ) + ], + ( + exists $version_filters->{must_not} + ? ( must_not => [ @{ $version_filters->{must_not} } ] ) + : () + ) + } + }; + + # filters to be applied to the nested modules + if ($module_filter) { + push @filters, + { + nested => { + path => 'module', + query => $entity_filter, + } + }; + } + else { + push @filters, $entity_filter; + } + + my $filter + = @filters + ? { bool => { must => \@filters } } + : $filters[0]; + + my $version_sort + = $module_filter + ? { + 'module.version_numified' => { + mode => 'max', + order => 'desc', + ( + $self->es->api_version ge '6_0' + ? ( + nested => { + path => 'module', + filter => $entity_filter, + }, + ) + : ( + nested_path => 'module', + nested_filter => $entity_filter, + ) + ), + } + } + : { version_numified => { order => 'desc' } }; + + # sort by score, then version desc, then date desc + my @sort = ( '_score', $version_sort, { date => { order => 'desc' } } ); + + my $query; + + if ($dev) { + $query = $filter; + } + else { + # if not dev, then prefer latest > cpan > backpan + $query = { + function_score => { + query => $filter, + score_mode => 'first', + boost_mode => 'replace', + functions => [ + { + filter => { term => { status => 'latest' } }, + weight => 3 + }, + { + filter => { term => { status => 'cpan' } }, + weight => 2 + }, + { filter => { match_all => {} }, weight => 1 }, + ] + } + }; + } + + my $body = { + query => $query, + size => 1, + sort => \@sort, + _source => [ qw( + checksum_md5 + checksum_sha256 + date + distribution + download_url + release + status + version + name + ) ], + }; + + my $res = $self->es->search( + es_doc_path( $module_filter ? 'file' : 'release' ), + body => $body, + search_type => 'dfs_query_then_fetch', + ); + + return unless hit_total($res); + + my @checksums; + + my $hit = $res->{hits}{hits}[0]; + my $source = $hit->{_source}; + my $release = $source->{release}; + + if ($release) { + my $checksums = $self->get_checksums($release); + @checksums = ( + ( + $checksums->{checksum_md5} + ? ( checksum_md5 => $checksums->{checksum_md5} ) + : () + ), + ( + $checksums->{checksum_sha256} + ? ( checksum_sha256 => $checksums->{checksum_sha256} ) + : () + ), + ); + } + + my $source_name = delete $source->{name}; + if ( !$module_filter ) { + $source->{release} = $source_name; + } + + my $module + = $hit->{inner_hits}{module} + ? $hit->{inner_hits}{module}{hits}{hits}[0]{_source} + : {}; + + return +{ %$source, %$module, @checksums, }; +} + +sub _version_filters { + my ( $self, $version, $field ) = @_; + + return () unless $version; + + if ( $version =~ s/^==\s*// ) { + return +{ + must => [ { + term => { + $field => $self->_numify($version) + } + } ] + }; + } + elsif ( $version =~ /^[<>!]=?\s*/ ) { + my %ops = qw(< lt <= lte > gt >= gte); + my ( %filters, %range, @exclusion ); + my @requirements = split /,\s*/, $version; + for my $r (@requirements) { + if ( $r =~ s/^([<>]=?)\s*// ) { + $range{ $ops{$1} } = $self->_numify($r); + } + elsif ( $r =~ s/\!=\s*// ) { + push @exclusion, $self->_numify($r); + } + } + + if ( keys %range ) { + $filters{must} + = [ { range => { $field => \%range } } ]; + } + + if (@exclusion) { + $filters{must_not} = []; + push @{ $filters{must_not} }, + map { +{ term => { $field => $self->_numify($_) } } } + @exclusion; + } + + return \%filters; + } + elsif ( $version !~ /\s/ ) { + return +{ + must => [ { + range => { + $field => { 'gte' => $self->_numify($version) } + }, + } ] + }; + } +} + +sub _numify { + my ( $self, $ver ) = @_; + $ver =~ s/_//g; + version->new($ver)->numify; +} + +sub predecessor { + my ( $self, $name ) = @_; + + my $res = $self->es->search( + es_doc_path('release'), + body => { + query => { + bool => { + must => [ { term => { distribution => $name } }, ], + must_not => [ { term => { status => 'latest' } }, ], + }, + }, + sort => [ { date => 'desc' } ], + size => 1, + }, + ); + my ($release) = $res->{hits}{hits}[0]; + return unless $release; + return $release->{_source}; +} + +sub find { + my ( $self, $name ) = @_; + + my $res = $self->es->search( + es_doc_path('release'), + body => { + query => { + bool => { + must => [ + { term => { distribution => $name } }, + { term => { status => 'latest' } }, + ], + }, + }, + sort => [ { date => 'desc' } ], + size => 1, + }, + ); + my ($file) = $res->{hits}{hits}[0]; + return undef unless $file; + return $file->{_source}; +} + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Query/Role/Common.pm b/lib/MetaCPAN/Query/Role/Common.pm new file mode 100644 index 000000000..bc86b311b --- /dev/null +++ b/lib/MetaCPAN/Query/Role/Common.pm @@ -0,0 +1,46 @@ +package MetaCPAN::Query::Role::Common; +use Moose::Role; + +use MetaCPAN::Types::TypeTiny qw( ES ); + +has es => ( + is => 'ro', + required => 1, + isa => ES, + coerce => 1, +); + +sub name { + my $self = shift; + my $class = ref $self || $self; + + $class =~ /^MetaCPAN::Query::([^:]+)$/ + or return undef; + return lc $1; +} + +has _in_query => ( + is => 'ro', + init_arg => 'query', + weak_ref => 1, +); + +has _gen_query => ( + is => 'ro', + lazy => 1, + init_arg => undef, + default => sub { + my $self = shift; + my $name = $self->name; + + require MetaCPAN::Query; + MetaCPAN::Query->new( + es => $self->es, + ( $name ? ( $name => $self ) : () ), + ); + }, +); + +sub query { $_[0]->_in_query // $_[0]->_gen_query } + +1; diff --git a/lib/MetaCPAN/Query/Search.pm b/lib/MetaCPAN/Query/Search.pm new file mode 100644 index 000000000..d5cf21897 --- /dev/null +++ b/lib/MetaCPAN/Query/Search.pm @@ -0,0 +1,397 @@ +package MetaCPAN::Query::Search; + +use MetaCPAN::Moose; + +use Const::Fast qw( const ); +use Hash::Merge qw( merge ); +use List::Util qw( min uniq ); +use Log::Contextual qw( :log :dlog ); +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Util qw( MAX_RESULT_WINDOW hit_total true false ); +use MooseX::StrictConstructor; + +with 'MetaCPAN::Query::Role::Common'; + +const my $RESULTS_PER_RUN => 200; +const my @ROGUE_DISTRIBUTIONS => qw( + Acme-DependOnEverything + Bundle-Everything + kurila + perl-5.005_02+apache1.3.3+modperl + perlbench + perl_debug + perl_mlb + pod2texi + spodcxx +); + +sub search_for_first_result { + my ( $self, $search_term ) = @_; + my $es_query = $self->build_query($search_term); + my $es_results = $self->run_query( file => $es_query ); + + my $data = $es_results->{hits}{hits}[0]; + return $data->{_source}; +} + +=head2 search_web + + search_web( $search_term, $from, $page_size, $collapsed ); + +- search_term: + - can be unqualified string e.g. 'paging' + - can be author e.g: 'author:LLAP' + - can be module e.g.: 'module:Data::Pageset' + - can be distribution e.g.: 'dist:Data-Pageset' + +- from: where in result set to start, int + +- page_size: number of results per page, int + +- collapsed: whether to merge results by dist or not + +=cut + +sub search_web { + my ( $self, $search_term, $page, $page_size, $collapsed, + $max_collapsed_hits ) + = @_; + $page_size //= 20; + $page //= 1; + + if ( $page * $page_size >= MAX_RESULT_WINDOW ) { + return { + results => [], + total => 0, + tool => 0, + colapsed => $collapsed ? true : false, + }; + } + + $search_term =~ s{([+=>_search_collapsed( $search_term, $page, $page_size, + $max_collapsed_hits ) + : $self->_search_expanded( $search_term, $page, $page_size ); + + return $results; +} + +sub _search_expanded { + my ( $self, $search_term, $page, $page_size ) = @_; + + # Used for distribution and module searches, the limit is included in + # the query and ES does the right thing (because we are not collapsing + # results by distribution). + my $es_query = $self->build_query( + $search_term, + { + size => $page_size, + from => ( $page - 1 ) * $page_size, + } + ); + + my $es_results = $self->run_query( file => $es_query ); + + # Extract results from es + my $results = $self->_extract_results($es_results); + + $results = [ + map { { + hits => [$_], + distribution => $_->{distribution}, + total => 1, + } } @$results + ]; + + my $return = { + results => $results, + total => hit_total($es_results), + took => $es_results->{took}, + collapsed => false, + }; + return $return; +} + +sub _search_collapsed { + my ( $self, $search_term, $page, $page_size, $max_collapsed_hits ) = @_; + + $max_collapsed_hits ||= 5; + + my $from = ( $page - 1 ) * $page_size; + my $total_size = $page * $page_size; + + my $es_query_opts = { + size => 0, + _source => [ qw( + ) ], + }; + + my $es_query = $self->build_query( $search_term, $es_query_opts ); + my $source = delete $es_query->{_source}; + + my $script_key = $self->es->api_version ge '5_0' ? 'source' : 'inline'; + + $es_query->{aggregations} = { + by_dist => { + terms => { + size => $total_size, + field => 'distribution', + order => { + max_score => 'desc', + }, + }, + aggregations => { + top_files => { + top_hits => { + _source => $source, + size => $max_collapsed_hits, + }, + }, + max_score => { + max => { + script => { + lang => "expression", + $script_key => "_score", + }, + }, + }, + }, + }, + total_dists => { + cardinality => { + field => 'distribution', + }, + }, + }; + + my $es_results = $self->run_query( file => $es_query ); + + my $output = { + results => [], + total => $es_results->{aggregations}{total_dists}{value}, + took => $es_results->{took}, + collapsed => true, + }; + + my $last = min( $total_size - 1, + $#{ $es_results->{aggregations}{by_dist}{buckets} } ); + my @dists = @{ $es_results->{aggregations}{by_dist}{buckets} } + [ $from .. $last ]; + + @{ $output->{results} } = map { + +{ + hits => $self->_extract_results( $_->{top_files} ), + distribution => $_->{key}, + total => $_->{doc_count}, + }; + } @dists; + + return $output; +} + +sub build_query { + my ( $self, $search_term, $params ) = @_; + $params //= {}; + ( my $clean = $search_term ) =~ s/::/ /g; + + my $query = { + bool => { + filter => [ + { term => { status => 'latest' } }, + { term => { authorized => true } }, + { term => { indexed => true } }, + { + bool => { + should => [ + { + bool => { + must => [ + { + exists => + { field => 'module.name' } + }, + { + term => + { 'module.indexed' => true } + } + ], + } + }, + { exists => { field => 'documentation' } }, + ], + } + }, + ], + must_not => [ { + terms => { + distribution => [ $self->query->distribution->rogue_list ] + } + } ], + must => [ + { + bool => { + should => [ + + # exact matches result in a huge boost + { + term => { + 'documentation' => { + value => $search_term, + boost => 20, + } + } + }, + { + term => { + 'module.name' => { + value => $search_term, + boost => 20, + } + } + }, + + # take the maximum score from the module name and the abstract/pod + { + dis_max => { + queries => [ + { + query_string => { + fields => [ + qw(documentation.analyzed^2 module.name.analyzed^2 distribution.analyzed), + qw(documentation.camelcase module.name.camelcase distribution.camelcase) + ], + query => $clean, + boost => 3, + default_operator => 'AND', + allow_leading_wildcard => + false, + + } + }, + { + query_string => { + fields => [ + qw(abstract.analyzed pod.analyzed) + ], + query => $clean, + default_operator => 'AND', + allow_leading_wildcard => + false, + }, + }, + ], + } + }, + ], + } + }, + ], + }, + }; + + my $script_key = $self->es->api_version ge '5_0' ? 'source' : 'inline'; + + $query = { + function_score => { + script_score => { + + # prefer shorter module names + script => { + lang => 'expression', + $script_key => + "_score - (doc['documentation_length'].value == 0 ? 26 : doc['documentation_length'].value)/400", + }, + }, + query => { + boosting => { + negative_boost => 0.5, + positive => $query, + negative => { + bool => { + should => [ + { + term => { 'mime' => 'text/x-script.perl' } + }, + { term => { 'deprecated' => true } }, + ], + }, + }, + }, + }, + }, + }; + + my $search = merge( + $params, + { + query => $query, + _source => [ qw( + module + abstract + author + authorized + date + description + dist_fav_count + distribution + documentation + id + indexed + path + pod_lines + release + status + ) ], + } + ); + + # Ensure our requested fields are unique so that Elasticsearch doesn't + # return us the same value multiple times in an unexpected arrayref. + $search->{_source} = [ uniq @{ $search->{_source} || [] } ]; + + return $search; +} + +sub run_query { + my ( $self, $doc, $es_query ) = @_; + return $self->es->search( + es_doc_path($doc), + body => $es_query, + search_type => 'dfs_query_then_fetch', + ); +} + +sub _extract_results { + my ( $self, $es_results ) = @_; + + return [ + map { + my $res = $_; + +{ + favorites => delete $res->{_source}->{dist_fav_count}, + %{ $res->{_source} }, + score => $res->{_score}, + } + } @{ $es_results->{hits}{hits} } + ]; +} + +1; + diff --git a/lib/MetaCPAN/Role/Common.pm b/lib/MetaCPAN/Role/Common.pm deleted file mode 100644 index 9e3adb577..000000000 --- a/lib/MetaCPAN/Role/Common.pm +++ /dev/null @@ -1,147 +0,0 @@ -package MetaCPAN::Role::Common; - -use Moose::Role; -use ElasticSearch; -use Log::Contextual qw( set_logger :dlog ); -use Log::Log4perl ':easy'; -use MetaCPAN::Types qw(:all); -use ElasticSearchX::Model::Document::Types qw(:all); -use MooseX::Types::Path::Class qw(:all); -use FindBin; -use MetaCPAN::Model; - -has 'cpan' => ( - is => 'rw', - isa => Dir, - lazy_build => 1, - coerce => 1, - documentation => - 'Location of a local CPAN mirror, looks for $ENV{MINICPAN} and ~/CPAN' -); - -has level => ( - is => 'ro', - isa => 'Str', - required => 1, - trigger => \&set_level, - documentation => 'Log level' -); - -has es => ( - isa => ES, - is => 'ro', - required => 1, - coerce => 1, - documentation => 'ElasticSearch http connection string' -); - -has model => ( lazy_build => 1, is => 'ro', traits => ['NoGetopt'] ); - -has index => ( - reader => '_index', - is => 'ro', - isa => 'Str', - default => 'cpan', - documentation => 'Index to use, defaults to "cpan"' -); - -has port => ( - isa => 'Int', - is => 'ro', - required => 1, - documentation => 'Port for the proxy, defaults to 5000' -); - -has logger => ( - is => 'ro', - required => 1, - isa => Logger, - coerce => 1, - predicate => 'has_logger', - traits => ['NoGetopt'] -); - -has home => ( - is => 'ro', - isa => Dir, - coerce => 1, - default => "$FindBin::RealBin/..", -); - -sub index { - my $self = shift; - return $self->model->index( $self->_index ); -} - -sub set_level { - my $self = shift; - $self->logger->level( - Log::Log4perl::Level::to_priority( uc( $self->level ) ) ); -} - -sub _build_model { - my $self = shift; - return MetaCPAN::Model->new( es => $self->es ); -} - -# NOT A MOOSE BUILDER -sub _build_logger { - my ($config) = @_; - my $log = Log::Log4perl->get_logger( $ARGV[0] ); - foreach my $c (@$config) { - my $layout = Log::Log4perl::Layout::PatternLayout->new( $c->{layout} - || "%d %p{1} %c: %m{chomp}%n" ); - my $app = Log::Log4perl::Appender->new( $c->{class}, %$c ); - $app->layout($layout); - $log->add_appender($app); - } - return $log; -} - -sub file2mod { - - my $self = shift; - my $name = shift; - - $name =~ s{\Alib\/}{}; - $name =~ s{\.(pod|pm)\z}{}; - $name =~ s{\/}{::}gxms; - - return $name; -} - -sub _build_cpan { - my $self = shift; - my @dirs - = ( "$ENV{'HOME'}/CPAN", "$ENV{'HOME'}/minicpan", $ENV{'MINICPAN'} ); - foreach my $dir ( grep {defined} @dirs ) { - return $dir if -d $dir; - } - die - "Couldn't find a local cpan mirror. Please specify --cpan or set MINICPAN"; - -} - -sub remote { - shift->es->transport->default_servers->[0]; -} - -sub run { } -before run => sub { - my $self = shift; - unless ($MetaCPAN::Role::Common::log) { - $MetaCPAN::Role::Common::log = $self->logger; - set_logger $self->logger; - } - Dlog_debug {"Connected to $_"} $self->remote; -}; - -1; - -=pod - -=head1 SYNOPSIS - -Roles which should be available to all modules - -=cut diff --git a/lib/MetaCPAN/Role/HasConfig.pm b/lib/MetaCPAN/Role/HasConfig.pm new file mode 100644 index 000000000..ad1bae0d2 --- /dev/null +++ b/lib/MetaCPAN/Role/HasConfig.pm @@ -0,0 +1,25 @@ +package MetaCPAN::Role::HasConfig; + +use Moose::Role; + +use MetaCPAN::Server::Config (); +use MetaCPAN::Types::TypeTiny qw( HashRef ); + +# Done like this so can be required by a role +sub config { + return $_[0]->_config; +} + +has _config => ( + is => 'ro', + isa => HashRef, + lazy => 1, + builder => '_build_config', +); + +sub _build_config { + my $self = shift; + return MetaCPAN::Server::Config::config(); +} + +1; diff --git a/lib/MetaCPAN/Role/HasRogueDistributions.pm b/lib/MetaCPAN/Role/HasRogueDistributions.pm new file mode 100644 index 000000000..14a02facc --- /dev/null +++ b/lib/MetaCPAN/Role/HasRogueDistributions.pm @@ -0,0 +1,25 @@ +package MetaCPAN::Role::HasRogueDistributions; + +use Moose::Role; + +use MetaCPAN::Types::TypeTiny qw( ArrayRef ); + +has rogue_distributions => ( + is => 'ro', + isa => ArrayRef, + default => sub { + [ qw( + Bundle-Everything + kurila + perl-5.005_02+apache1.3.3+modperl + perlbench + perl_debug + perl_mlb + pod2texi + spodcxx + ) ]; + }, +); + +no Moose::Role; +1; diff --git a/lib/MetaCPAN/Role/Logger.pm b/lib/MetaCPAN/Role/Logger.pm new file mode 100644 index 000000000..95d80d662 --- /dev/null +++ b/lib/MetaCPAN/Role/Logger.pm @@ -0,0 +1,73 @@ +package MetaCPAN::Role::Logger; + +use v5.10; +use Moose::Role; + +use Log::Contextual qw( set_logger ); +use Log::Log4perl ':easy'; +use MetaCPAN::Types::TypeTiny qw( Logger Str ); +use MooseX::Getopt (); ## no perlimports +use Path::Tiny qw( path ); + +has level => ( + is => 'ro', + isa => Str, + required => 1, + trigger => \&set_level, + documentation => 'Log level', +); + +has logger => ( + is => 'ro', + required => 1, + isa => Logger, + coerce => 1, + traits => ['NoGetopt'], +); + +sub set_level { + my $self = shift; + $self->logger->level( + Log::Log4perl::Level::to_priority( uc( $self->level ) ) ); +} + +# NOTE: This makes the test suite print "mapping" regardless of which +# script class is actually running (the category only gets set once) +# but Log::Contextual gets mad if you call set_logger more than once. +sub set_logger_once { + state $logger_set = 0; + return if $logger_set; + + my $self = shift; + + set_logger $self->logger; + + $logger_set = 1; + + return; +} + +# Not actually a Moose builder, so we should probably rename it. +sub _build_logger { + my ($config) = @_; + my $log = Log::Log4perl->get_logger( $ARGV[0] + || 'this_would_have_been_argv_0_but_there_is_no_such_thing' ); + foreach my $c (@$config) { + my $layout = Log::Log4perl::Layout::PatternLayout->new( $c->{layout} + || qq{%d %p{1} %c: %m{chomp}%n} ); + + if ( $c->{class} =~ /Appender::File$/ && $c->{filename} ) { + + # Create the log file's parent directory if necessary. + path( $c->{filename} )->parent->mkpath; + } + + my $app = Log::Log4perl::Appender->new( $c->{class}, %$c ); + + $app->layout($layout); + $log->add_appender($app); + } + return $log; +} + +1; diff --git a/lib/MetaCPAN/Role/Script.pm b/lib/MetaCPAN/Role/Script.pm new file mode 100644 index 000000000..3ed00e450 --- /dev/null +++ b/lib/MetaCPAN/Role/Script.pm @@ -0,0 +1,300 @@ +package MetaCPAN::Role::Script; + +use Moose::Role; + +use Carp (); +use IO::Prompt::Tiny qw( prompt ); +use Log::Contextual qw( :log :dlog ); +use MetaCPAN::Model (); +use MetaCPAN::Types::TypeTiny qw( AbsPath Bool ES HashRef Int Path Str ); +use MetaCPAN::Util qw( root_dir ); +use Mojo::Server (); +use Term::ANSIColor qw( colored ); +use MetaCPAN::Model::ESWrapper (); + +use MooseX::Getopt::OptionTypeMap (); +for my $type ( Path, AbsPath, ES ) { + MooseX::Getopt::OptionTypeMap->add_option_type_to_map( $type, '=s' ); +} + +with( 'MetaCPAN::Role::HasConfig', 'MetaCPAN::Role::Fastly', + 'MetaCPAN::Role::Logger' ); + +has cpan => ( + is => 'ro', + isa => Path, + lazy => 1, + builder => '_build_cpan', + coerce => 1, + documentation => + 'Location of a local CPAN mirror, looks for $ENV{MINICPAN} and ~/CPAN', +); + +has cpan_file_map => ( + is => 'ro', + isa => HashRef, + lazy => 1, + builder => '_build_cpan_file_map', + traits => ['NoGetopt'], +); + +has die_on_error => ( + is => 'ro', + isa => Bool, + default => 0, + documentation => 'Die on errors instead of simply logging', +); + +has exit_code => ( + isa => Int, + is => 'rw', + default => 0, + documentation => 'Exit Code to be returned on termination', +); + +has ua => ( + is => 'ro', + lazy => 1, + builder => '_build_ua', +); + +has proxy => ( + is => 'ro', + isa => Str, + default => '', +); + +has es => ( + is => 'ro', + isa => ES, + required => 1, + init_arg => 'elasticsearch_servers', + coerce => 1, + documentation => 'Elasticsearch http connection string', +); + +has model => ( + is => 'ro', + init_arg => undef, + lazy => 1, + builder => '_build_model', + traits => ['NoGetopt'], +); + +has port => ( + isa => Int, + is => 'ro', + required => 0, + lazy => 1, + default => sub {5000}, + documentation => 'Port for the proxy, defaults to 5000', +); + +has home => ( + is => 'ro', + isa => Path, + lazy => 1, + coerce => 1, + default => sub { root_dir() }, +); + +has _minion => ( + is => 'ro', + isa => 'Minion', + lazy => 1, + handles => { _add_to_queue => 'enqueue', stats => 'stats', }, + default => sub { Mojo::Server->new->build_app('MetaCPAN::API')->minion }, +); + +has queue => ( + is => 'ro', + isa => Bool, + default => 0, + documentation => 'add indexing jobs to the minion queue', +); + +sub handle_error { + my ( $self, $error, $die_always ) = @_; + + # Die if configured (for the test suite). + $die_always = $self->die_on_error unless defined $die_always; + + # Always log. + log_fatal {$error}; + + $! = $self->exit_code if ( $self->exit_code != 0 ); + + Carp::croak $error if $die_always; +} + +sub print_error { + my ( $self, $error ) = @_; + + log_error {$error}; +} + +sub _build_model { + my $self = shift; + + # es provided by ElasticSearchX::Model::Role + + my $es = MetaCPAN::Model::ESWrapper->new( $self->es ); + return MetaCPAN::Model->new( es => $es ); +} + +sub _build_ua { + my $self = shift; + my $ua = LWP::UserAgent->new; + my $proxy = $self->proxy; + + if ($proxy) { + $proxy eq 'env' + ? $ua->env_proxy + : $ua->proxy( [qw], $proxy ); + } + + $ua->agent('MetaCPAN'); + + return $ua; +} + +sub _build_cpan { + my $self = shift; + my @dirs = ( + $ENV{MINICPAN}, '/home/metacpan/CPAN', + "$ENV{HOME}/CPAN", "$ENV{HOME}/minicpan", + ); + foreach my $dir ( grep {defined} @dirs ) { + return $dir if -d $dir; + } + die + "Couldn't find a local cpan mirror. Please specify --cpan or set MINICPAN"; + +} + +sub _build_cpan_file_map { + my $self = shift; + my $ls = $self->cpan->child(qw(indices find-ls.gz)); + unless ( -e $ls ) { + die "File $ls does not exist"; + } + log_info {"Reading $ls"}; + my $cpan = {}; + open my $fh, "<:gzip", $ls; + while (<$fh>) { + my $path = ( split(/\s+/) )[-1]; + next unless ( $path =~ /^authors\/id\/\w+\/\w+\/(\w+)\/(.*)$/ ); + $cpan->{$1}{$2} = 1; + } + close $fh; + return $cpan; +} + +sub run { } +before run => sub { + my $self = shift; + $self->set_logger_once; +}; + +sub are_you_sure { + my ( $self, $msg ) = @_; + my $iconfirmed = 0; + + if ( -t *STDOUT ) { + my $answer + = prompt colored( ['bold red'], "*** Warning ***: $msg" ) . "\n" + . 'Are you sure you want to do this (type "YES" to confirm) ? '; + if ( $answer ne 'YES' ) { + log_error {"Confirmation incorrect: '$answer'"}; + print "Operation will be interruped!\n"; + + #Set System Error: 125 - ECANCELED - Operation canceled + $self->exit_code(125); + $self->handle_error( 'Operation canceled on User Request', 1 ); + } + else { + log_info {'Operation confirmed.'}; + print "alright then...\n"; + $iconfirmed = 1; + } + } + else { + log_info {"*** Warning ***: $msg"}; + $iconfirmed = 1; + } + + return $iconfirmed; +} + +before perform_purges => sub { + my ($self) = @_; + if ( $self->has_surrogate_keys_to_purge ) { + log_info { + "CDN Purge: " . join ', ', $self->surrogate_keys_to_purge; + }; + } +}; + +1; + +__END__ + +=pod + +=head1 NAME + +MetaCPAN::Role::Script - Base Role which is used by many command line applications + +=head1 SYNOPSIS + +Roles which should be available to all modules. + +=head1 OPTIONS + +This Role makes the command line application accept the following options + +=over 4 + +=item Option C<--await 15> + +This option will set the I. +After C seconds the Application will fail with an Exception and the Exit Code [112] +(C<112 - EHOSTDOWN - Host is down>) will be returned + + bin/metacpan --await 15 + +B If the I service does not become available +within C seconds it exits the Script with Exit Code C< 112 >. + +See L> + +=back + +=head1 METHODS + +This Role provides the following methods + +=over 4 + +=item C + +Requests the user to confirm the operation with "I< YES >" + +B When the operator input does not match "I< YES >" it will exit the Script +with Exit Code [125] (C<125 - ECANCELED - Operation canceled>). + +=item C + +Logs the string C with the log function as fatal error. +If C is not equel C< 0 > sets its value in C< $! >. +If the option C<--die_on_error> is enabled it throws an Exception with C. +If the parameter C is set it overrides the option C<--die_on_error>. + +=item C + +Logs the string C with the log function and displays it in red. +But it does not end the application. + +=back + +=cut diff --git a/lib/MetaCPAN/Script/Author.pm b/lib/MetaCPAN/Script/Author.pm index 3af67a54d..00e59e372 100644 --- a/lib/MetaCPAN/Script/Author.pm +++ b/lib/MetaCPAN/Script/Author.pm @@ -1,18 +1,22 @@ package MetaCPAN::Script::Author; +use strict; +use warnings; + use Moose; -with 'MooseX::Getopt'; -use Log::Contextual qw( :log ); -with 'MetaCPAN::Role::Common'; -use Email::Valid (); -use File::stat (); -use JSON::XS (); -use URI (); -use Encode (); -use XML::Simple qw(XMLin); -use DateTime::Format::ISO8601 (); - -use MetaCPAN::Document::Author; +with 'MooseX::Getopt', 'MetaCPAN::Role::Script'; + +use Cpanel::JSON::XS qw( decode_json ); +use DateTime (); +use Email::Valid (); +use Encode (); +use Log::Contextual qw( :log :dlog ); +use MetaCPAN::Document::Author (); +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Types::TypeTiny qw( Str ); +use MetaCPAN::Util qw(diff_struct true false); +use URI (); +use XML::XPath (); =head1 SYNOPSIS @@ -20,112 +24,302 @@ Loads author info into db. Requires the presence of a local CPAN/minicpan. =cut -has 'author_fh' => ( - is => 'rw', +has author_fh => ( + is => 'ro', traits => ['NoGetopt'], - default => sub { shift->cpan . "/authors/00whois.xml" } + lazy => 1, + default => sub { shift->cpan . '/authors/00whois.xml' }, +); + +has pauseid => ( + is => 'ro', + isa => Str, ); sub run { my $self = shift; + $self->index_authors; - $self->index->refresh; + $self->es->indices->refresh; +} + +my @author_config_fields = qw( + name + asciiname + profile + blog + perlmongers + donation + email + website + city + region + country + location + extra +); + +my @cpan_fields = qw( + pauseid + name + email + website + asciiname + is_pause_custodial_account +); + +my @compare_fields = do { + my %seen; + sort grep !$seen{$_}++, @cpan_fields, @author_config_fields; +}; + +has whois_data => ( + is => 'ro', + traits => ['NoGetopt'], + lazy => 1, + builder => '_build_whois_data', +); + +sub _build_whois_data { + my $self = shift; + + my $whois_data = {}; + + my $xp = XML::XPath->new( filename => $self->author_fh ); + + for my $author ( $xp->find('/cpan-whois/cpanid')->get_nodelist ) { + my $data = { + map +( $_->getLocalName, $_->string_value ), + grep $_->isa('XML::XPath::Node::Element'), + $author->getChildNodes + }; + + my $pauseid = $data->{id}; + my $existing = $whois_data->{$pauseid}; + if ( !$existing + || $existing->{type} eq 'author' && $data->{type} eq 'list' ) + { + $whois_data->{$pauseid} = $data; + } + } + + return $whois_data; } sub index_authors { my $self = shift; - my $type = $self->index->type('author'); - my $authors = XMLin( $self->author_fh )->{cpanid}; - my $count = keys %$authors; - log_debug {"Counting author"}; - log_info {"Indexing $count authors"}; - - log_debug {"Getting last update dates"}; - my $dates - = $type->inflate(0)->filter( { exists => { field => 'updated' } } ) - ->size(99999)->all; - $dates = { - map { - $_->{pauseid} => - DateTime::Format::ISO8601->parse_datetime( $_->{updated} ) - } map { $_->{_source} } @{ $dates->{hits}->{hits} } - }; + my $authors = $self->whois_data; - my $bulk = $self->model->bulk( size => 500 ); - - while ( my ( $pauseid, $data ) = each %$authors ) { - my ( $name, $email, $homepage, $asciiname ) - = ( @$data{qw(fullname email homepage asciiname)} ); - $name = undef if ( ref $name ); - $email = lc($pauseid) . '@cpan.org' - unless ( $email && Email::Valid->address($email) ); - log_debug { - Encode::encode_utf8( - sprintf( "Indexing %s: %s <%s>", $pauseid, $name, $email ) ); - }; - my $conf = $self->author_config( $pauseid, $dates ) || next; - my $put = { - pauseid => $pauseid, - name => $name, - asciiname => ref $asciiname ? undef : $asciiname, - email => $email, - website => $homepage, - map { $_ => $conf->{$_} } - grep { defined $conf->{$_} } keys %$conf - }; - $put->{website} = [ $put->{website} ] - unless ( ref $put->{website} eq 'ARRAY' ); - $put->{website} = [ - - # normalize www.homepage.com to http://www.homepage.com - map { $_->scheme ? $_->as_string : 'http://' . $_->as_string } - map { URI->new($_)->canonical } - grep {$_} @{ $put->{website} } - ]; - my $author = $type->new_document($put); - $author->gravatar_url; # build gravatar_url - $bulk->put($author); + if ( $self->pauseid ) { + log_info {"Indexing 1 author"}; + $authors = { $self->pauseid => $authors->{ $self->pauseid } }; + } + else { + my $count = keys %$authors; + log_debug {"Counting author"}; + log_info {"Indexing $count authors"}; + } + + my @author_ids_to_purge; + + my $bulk = $self->es->bulk_helper( + es_doc_path('author'), + max_count => 250, + timeout => '25m', + ); + + my $scroll = $self->es->scroll_helper( + es_doc_path('author'), + size => 500, + body => { + query => { + $self->pauseid + ? ( + term => { + pauseid => $self->pauseid, + }, + ) + : ( match_all => {} ), + }, + _source => [@compare_fields], + sort => '_doc', + }, + ); + + # update authors + while ( my $doc = $scroll->next ) { + my $pauseid = $doc->{_id}; + my $whois_data = delete $authors->{$pauseid} || next; + $self->update_author( $bulk, $pauseid, $whois_data, $doc->{_source} ); + } + + # new authors + for my $pauseid ( keys %$authors ) { + my $whois_data = delete $authors->{$pauseid} || next; + $self->update_author( $bulk, $pauseid, $whois_data ); } - $self->index->refresh; + + $bulk->flush; + $self->es->indices->refresh; + + $self->perform_purges; + log_info {"done"}; } -sub author_config { - my ( $self, $pauseid, $dates ) = @_; - my $fallback = $dates->{$pauseid} ? undef : {}; - my $dir = $self->cpan->subdir( 'authors', - MetaCPAN::Util::author_dir($pauseid) ); - my @files; - opendir( my $dh, $dir ) || return {}; - my ($file) - = sort { $dir->file($b)->stat->mtime <=> $dir->file($a)->stat->mtime } - grep {m/author-.*?\.json/} readdir($dh); - return $fallback unless ($file); - $file = $dir->file($file); - return $fallback if !-e $file; - my $mtime = DateTime->from_epoch( epoch => $file->stat->mtime ); - - if ( $dates->{$pauseid} && $dates->{$pauseid} >= $mtime ) { - log_debug {"Skipping $pauseid (newer version in index)"}; - return undef; +sub author_data_from_cpan { + my $self = shift; + my ( $pauseid, $whois_data ) = @_; + + my $author_config = $self->author_config($pauseid) || {}; + + my $data = { + pauseid => $pauseid, + name => $whois_data->{fullname}, + email => $whois_data->{email}, + website => $whois_data->{homepage}, + asciiname => $whois_data->{asciiname}, + %$author_config, + is_pause_custodial_account => ( + ( $whois_data->{fullname} // '' ) + =~ /\(PAUSE Custodial Account\)/ ? true : false + ), + }; + + undef $data->{name} + if ref $data->{name}; + + if ( !length $data->{name} ) { + $data->{name} = $pauseid; } - my $json = $file->slurp; - my $author = eval { JSON::XS->new->utf8->relaxed->decode($json) }; - if ($@) { - log_warn {"$file is broken: $@"}; - return $fallback; + $data->{asciiname} = q{} + if !defined $data->{asciiname}; + + $data->{email} = lc($pauseid) . '@cpan.org' + unless $data->{email} && Email::Valid->address( $data->{email} ); + + $data->{website} = [ + + # normalize www.homepage.com to http://www.homepage.com + map +( $_->scheme ? '' : 'http://' ) . $_->as_string, + map URI->new($_)->canonical, + grep $_, + map +( ref eq 'ARRAY' ? @$_ : $_ ), + $data->{website} + ]; + + # Do not import lat / lon's in the wrong order, or just invalid + if ( my $loc = $data->{location} ) { + if ( ref $loc ne 'ARRAY' || @$loc != 2 ) { + delete $data->{location}; + } + else { + my $lat = $loc->[1]; + my $lon = $loc->[0]; + + if ( !defined $lat or $lat > 90 or $lat < -90 ) { + + # Invalid latitude + delete $data->{location}; + } + elsif ( !defined $lon or $lon > 180 or $lon < -180 ) { + + # Invalid longitude + delete $data->{location}; + } + } + } + + return $data; +} + +sub update_author { + my $self = shift; + my ( $bulk, $pauseid, $whois_data, $current_data ) = @_; + + my $data = $self->author_data_from_cpan( $pauseid, $whois_data ); + + log_debug { + Encode::encode_utf8( sprintf( + "Indexing %s: %s <%s>", + $pauseid, $data->{name}, $data->{email} + ) ); + }; + + # Now check the format we have is actually correct + if ( my @errors = MetaCPAN::Document::Author->validate($data) ) { + Dlog_error { + "Invalid data for $pauseid: $_" + } + \@errors; + return; + } + + if ( my $diff = diff_struct( $current_data, $data, 1 ) ) { + + # log a sampling of differences + if ( $self->has_surrogate_keys_to_purge % 10 == 9 ) { + Dlog_debug { + "Found difference in $pauseid: $_" + } + $diff; + } } else { - $author - = { map { $_ => $author->{$_} } - qw(name asciiname profile blog perlmongers donation email website city region country location extra) - }; - $author->{updated} = $mtime; - return $author; + return; } + + $data->{updated} = DateTime->now( time_zone => 'UTC' )->iso8601; + + $bulk->update( { + id => $pauseid, + doc => $data, + doc_as_upsert => true, + } ); + + $self->purge_author_key($pauseid); +} + +sub author_config { + my ( $self, $pauseid ) = @_; + + my $dir = $self->cpan->child( 'authors', + MetaCPAN::Util::author_dir($pauseid) ); + + return undef + unless $dir->is_dir; + + my $author_cpan_files = $self->cpan_file_map->{$pauseid} + or return undef; + + # Get the most recent version + my ($file) = map $_->[0], sort { $b->[1] <=> $a->[1] } + map [ $_ => $_->stat->mtime ], + grep $author_cpan_files->{ $_->basename }, + $dir->children(qr/\Aauthor-.*\.json\z/); + + return undef + unless $file; + + my $author; + eval { + $author = decode_json( $file->slurp_raw ); + 1; + } or do { + log_warn {"$file is broken: $@"}; + return undef; + }; + + return { + map { + my $value = $author->{$_}; + defined $value ? ( $_ => $value ) : () + } @author_config_fields + }; } +__PACKAGE__->meta->make_immutable; 1; =pod diff --git a/lib/MetaCPAN/Script/Backpan.pm b/lib/MetaCPAN/Script/Backpan.pm new file mode 100644 index 000000000..935ed4a36 --- /dev/null +++ b/lib/MetaCPAN/Script/Backpan.pm @@ -0,0 +1,208 @@ +package MetaCPAN::Script::Backpan; + +use strict; +use warnings; + +use Moose; + +use Log::Contextual qw( :log :dlog ); +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Types::TypeTiny qw( Bool HashRef Str ); + +with 'MetaCPAN::Role::Script', 'MooseX::Getopt::Dashes'; + +has distribution => ( + is => 'ro', + isa => Str, + documentation => 'work on given distribution', +); + +has undo => ( + is => 'ro', + isa => Bool, + default => 0, + documentation => 'mark releases as status=cpan', +); + +has files_only => ( + is => 'ro', + isa => Bool, + default => 0, + documentation => 'only update the "file" index', +); + +has _release_status => ( + is => 'ro', + isa => HashRef, + default => sub { +{} }, +); + +has _bulk => ( + is => 'ro', + isa => HashRef, + default => sub { +{} }, +); + +sub run { + my $self = shift; + + $self->es->trace_calls(1) if $ENV{DEBUG}; + + $self->build_release_status_map(); + + $self->update_releases() unless $self->files_only; + + $self->update_files(); + + $_->flush for values %{ $self->_bulk }; +} + +sub build_release_status_map { + my $self = shift; + + log_info {"find_releases"}; + + my $scroll = $self->es->scroll_helper( + scroll => '5m', + es_doc_path('release'), + body => { + %{ $self->_get_release_query }, + size => 500, + _source => [ 'author', 'archive', 'name' ], + }, + ); + + while ( my $release = $scroll->next ) { + my $author = $release->{_source}{author}; + my $archive = $release->{_source}{archive}; + my $name = $release->{_source}{name}; + next unless $name; # bypass some broken releases + + $self->_release_status->{$author}{$name} = [ + ( + $self->undo + or exists $self->cpan_file_map->{$author}{$archive} + ) + ? 'cpan' + : 'backpan', + $release->{_id} + ]; + } +} + +sub _get_release_query { + my $self = shift; + + unless ( $self->undo ) { + return +{ + query => { + bool => + { must_not => [ { term => { status => 'backpan' } } ] } + } + }; + } + + return +{ + query => { + bool => { + must => [ + { term => { status => 'backpan' } }, + ( + $self->distribution + ? { + term => { distribution => $self->distribution } + } + : () + ) + ] + } + } + }; +} + +sub update_releases { + my $self = shift; + + log_info {"update_releases"}; + + $self->_bulk->{release} ||= $self->es->bulk_helper( + es_doc_path('release'), + max_count => 250, + timeout => '5m', + ); + + for my $author ( keys %{ $self->_release_status } ) { + + # value = [ status, _id ] + for ( values %{ $self->_release_status->{$author} } ) { + $self->_bulk->{release}->update( { + id => $_->[1], + doc => { + status => $_->[0], + } + } ); + } + } +} + +sub update_files { + my $self = shift; + + for my $author ( keys %{ $self->_release_status } ) { + my @releases = keys %{ $self->_release_status->{$author} }; + while ( my @chunk = splice @releases, 0, 1000 ) { + $self->update_files_author( $author, \@chunk ); + } + } +} + +sub update_files_author { + my $self = shift; + my $author = shift; + my $author_releases = shift; + + log_info { "update_files: " . $author }; + + my $scroll = $self->es->scroll_helper( + scroll => '5m', + es_doc_path('file'), + body => { + query => { + bool => { + must => [ + { term => { author => $author } }, + { terms => { release => $author_releases } } + ] + } + }, + size => 500, + _source => ['release'], + }, + ); + + $self->_bulk->{file} ||= $self->es->bulk_helper( + es_doc_path('file'), + max_count => 250, + timeout => '5m', + ); + my $bulk = $self->_bulk->{file}; + + while ( my $file = $scroll->next ) { + my $release = $file->{_source}{release}; + $bulk->update( { + id => $file->{_id}, + doc => { + status => $self->_release_status->{$author}{$release}[0] + } + } ); + } +} + +__PACKAGE__->meta->make_immutable; +1; + +=pod + +Sets "backpan" status on all BackPAN releases. + +=cut diff --git a/lib/MetaCPAN/Script/Backup.pm b/lib/MetaCPAN/Script/Backup.pm index e28d5fdc3..2ced44ea0 100644 --- a/lib/MetaCPAN/Script/Backup.pm +++ b/lib/MetaCPAN/Script/Backup.pm @@ -1,113 +1,214 @@ package MetaCPAN::Script::Backup; +use strict; +use warnings; +use feature qw( state ); + +use Cpanel::JSON::XS qw( decode_json encode_json ); +use DateTime (); +use IO::Zlib (); +use Log::Contextual qw( :log :dlog ); +use MetaCPAN::Types::TypeTiny qw( Bool CommaSepOption Int Path Str ); +use MetaCPAN::Util qw( true false ); +use MetaCPAN::ESConfig qw( es_config ); use Moose; -with 'MooseX::Getopt'; -use Log::Contextual qw( :log :dlog ); -with 'MetaCPAN::Role::Common'; -use MooseX::Types::Path::Class qw(:all); -use IO::Zlib (); -use JSON::XS; -use DateTime; +use Try::Tiny qw( catch try ); + +with 'MetaCPAN::Role::Script', 'MooseX::Getopt::Dashes'; + +has batch_size => ( + is => 'ro', + isa => Int, + default => 100, + documentation => + 'Number of documents to restore in one batch, defaults to 100', +); + +has index => ( + reader => '_index', + is => 'ro', + isa => CommaSepOption, + coerce => 1, + default => sub { es_config->all_indexes }, + documentation => 'ES indexes to backup, defaults to "' + . join( ', ', @{ es_config->all_indexes } ) . '"', +); has type => ( is => 'ro', - isa => 'Str', - documentation => 'ES type do backup, optional' + isa => Str, + documentation => 'ES type do backup, optional', ); has size => ( is => 'ro', - isa => 'Int', + isa => Int, default => 1000, - documentation => 'Size of documents to fetch at once, defaults to 1000' + documentation => 'Size of documents to fetch at once, defaults to 1000', ); has purge => ( is => 'ro', - isa => 'Bool', - documentation => 'Purge old backups' + isa => Bool, + documentation => 'Purge old backups', ); has dry_run => ( is => 'ro', - isa => 'Bool', - documentation => 'Don\'t actually purge old backups' + isa => Bool, + documentation => q{Don't actually purge old backups}, ); has restore => ( is => 'ro', - isa => File, + isa => Path, coerce => 1, documentation => 'Restore a backup', ); sub run { my $self = shift; - return $self->run_purge if ( $self->purge ); - return $self->run_restore if ( $self->restore ); + + return $self->run_purge if $self->purge; + return $self->run_restore if $self->restore; + my $es = $self->es; - $self->index->refresh; - my $filename = join( "-", - DateTime->now->strftime("%F"), - grep {defined} $self->index->name, - $self->type ); - my $file = $self->home->subdir(qw(var backup))->file("$filename.json.gz"); - $file->dir->mkpath unless ( -e $file->dir ); - my $fh = IO::Zlib->new( "$file", "wb4" ); - my $scroll = $es->scrolled_search( - index => $self->index->name, - $self->type ? ( type => $self->type ) : (), - size => $self->size, - search_type => 'scan', - fields => [qw(_parent _source)], - scroll => '1m', - ); - log_info { "Backing up ", $scroll->total, " documents" }; - while ( my $result = $scroll->next ) { - print $fh encode_json($result), $/; + for my $index ( @{ $self->_index } ) { + + $self->es->indices->refresh( index => $index ); + + my $filename = join( '-', + DateTime->now->strftime('%F'), + grep {defined} $index, + $self->type ); + + my $file = $self->home->child( qw(var backup), "$filename.json.gz" ); + $file->parent->mkpath unless ( -e $file->parent ); + my $fh = IO::Zlib->new( "$file", 'wb4' ); + + my $scroll = $es->scroll_helper( + index => $index, + $self->type ? ( type => $self->type ) : (), + scroll => '1m', + body => { + _source => true, + size => $self->size, + sort => '_doc', + }, + ); + + log_info { 'Backing up ', $scroll->total, ' documents' }; + + while ( my $result = $scroll->next ) { + print $fh encode_json($result), $/; + } + close $fh; } - close $fh; - log_info {"done"}; + log_info {'done'}; } sub run_restore { my $self = shift; - return log_fatal { $self->restore, " doesn't exist" } - unless ( -e $self->restore ); - log_info { "Restoring from ", $self->restore }; + + return log_fatal { $self->restore, q{ doesn't exist} } + unless ( -e $self->restore ); + log_info { 'Restoring from ', $self->restore }; + my @bulk; my $es = $self->es; - my $fh = IO::Zlib->new( $self->restore->stringify, "rb" ); + my $fh = IO::Zlib->new( $self->restore->stringify, 'rb' ); + + my %bulk_store; + while ( my $line = $fh->readline ) { - my $obj = decode_json($line); - my $parent = $obj->{fields}->{_parent}; - push( - @bulk, - { id => $obj->{_id}, - $parent ? ( parent => $parent ) : (), - index => $obj->{_index}, - type => $obj->{_type}, - data => $obj->{_source}, + + state $line_count = 0; + ++$line_count; + my $raw; + + try { $raw = decode_json($line) } + catch { + log_warn {"cannot decode JSON: $line --- $&"}; + }; + + # Create our bulk_helper if we need, + # incase a backup has mixed _index or _type + # create a new bulk helper for each + my $bulk_key = $raw->{_index} . $raw->{_type}; + + $bulk_store{$bulk_key} ||= $es->bulk_helper( + index => $raw->{_index}, + type => $raw->{_type}, + max_count => $self->batch_size + ); + + # Fetch relevant bulk helper + my $bulk = $bulk_store{$bulk_key}; + + my $parent = $raw->{_parent}; + + if ( $raw->{_type} eq 'author' ) { + + # Hack for dodgy lat / lon's + if ( my $loc = $raw->{_source}->{location} ) { + + my $lat = $loc->[1]; + my $lon = $loc->[0]; + + if ( $lat > 90 or $lat < -90 ) { + + # Invalid latitude + delete $raw->{_source}->{location}; + } + elsif ( $lon > 180 or $lon < -180 ) { + + # Invalid longitude + delete $raw->{_source}->{location}; + } } + } + + my $exists = $es->exists( + index => $raw->{_index}, + type => $raw->{_type}, + id => $raw->{_id}, ); - if ( @bulk > 100 ) { - $es->bulk_index( \@bulk ); - @bulk = (); + + if ($exists) { + $bulk->update( { + id => $raw->{_id}, + doc => $raw->{_source}, + doc_as_upsert => true, + } ); + } + else { + $bulk->create( { + id => $raw->{_id}, + $parent ? ( parent => $parent ) : (), + source => $raw->{_source}, + } ); + } + } + + # Flush anything left over just incase + for my $bulk ( values %bulk_store ) { + $bulk->flush; } - $es->bulk_index( \@bulk ); - log_info { "done"}; + log_info {'done'}; } sub run_purge { my $self = shift; - my $now = DateTime->now; - $self->home->subdir(qw(var backup))->recurse( - callback => sub { + + my $now = DateTime->now; + $self->home->child(qw(var backup))->visit( + sub { my $file = shift; return if ( $file->is_dir ); + my $mtime = DateTime->from_epoch( epoch => $file->stat->mtime ); # keep a daily backup for one week @@ -118,15 +219,17 @@ sub run_purge { if ( $mtime->clone->truncate( to => 'week' ) != $mtime->clone->truncate( to => 'day' ) ) { - log_info {"Removing old backup $file"}; - return log_info {"Not (dry run)"} + log_info {"Removing old backup $file"}; + return log_info {'Not (dry run)'} if ( $self->dry_run ); $file->remove; } - } + }, + { recurse => 1 } ); } +__PACKAGE__->meta->make_immutable; 1; __END__ @@ -138,9 +241,9 @@ MetaCPAN::Script::Backup - Backup indices and types =head1 SYNOPSIS $ bin/metacpan backup --index user --type account - + $ bin/metacpan backup --purge - + =head1 DESCRIPTION Creates C<.json.gz> files in C. These files contain diff --git a/lib/MetaCPAN/Script/CPANTesters.pm b/lib/MetaCPAN/Script/CPANTesters.pm index 80fddafe9..ebf630ae8 100644 --- a/lib/MetaCPAN/Script/CPANTesters.pm +++ b/lib/MetaCPAN/Script/CPANTesters.pm @@ -1,102 +1,169 @@ package MetaCPAN::Script::CPANTesters; use Moose; -with 'MooseX::Getopt'; -use Log::Contextual qw( :log :dlog ); -with 'MetaCPAN::Role::Common'; -use File::Spec::Functions qw(catfile); -use File::Temp qw(tempdir); -use File::stat qw(stat); -use LWP::UserAgent (); -use IO::Uncompress::Bunzip2 qw(bunzip2); -use DBI (); + +use DBI (); +use ElasticSearchX::Model::Document::Types qw( ESBulk ); +use File::stat qw( stat ); +use IO::Uncompress::Bunzip2 qw( bunzip2 ); +use Log::Contextual qw( :log :dlog ); +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Types::TypeTiny qw( Bool Path Uri ); +use MetaCPAN::Util qw( true false ); + +with 'MetaCPAN::Role::Script', 'MooseX::Getopt::Dashes'; has db => ( is => 'ro', - default => 'http://devel.cpantesters.org/release/release.db.bz2' + isa => Uri, + lazy => 1, + coerce => 1, + builder => '_build_db', +); + +has force_refresh => ( + is => 'ro', + isa => Bool, + default => 0, +); + +# XXX move path to config +has mirror_file => ( + is => 'ro', + isa => Path, + default => sub { + shift->home->child( 'var', ( $ENV{HARNESS_ACTIVE} ? 't' : () ), + 'tmp', 'cpantesters.db' ); + }, + coerce => 1, +); + +has skip_download => ( + is => 'ro', + isa => Bool, ); +has _bulk => ( + is => 'ro', + isa => ESBulk, + lazy => 1, + default => sub { + $_[0]->es->bulk_helper( es_doc_path('release') ); + }, +); + +# XXX fix hardcoded path +sub _build_db { + my $self = shift; + return $ENV{HARNESS_ACTIVE} + ? $self->home->child('t/var/cpantesters-release-fake.db.bz2') + : 'http://devel.cpantesters.org/release/release.db.bz2'; +} + sub run { my $self = shift; $self->index_reports; - $self->index->refresh; + $self->es->indices->refresh; } sub index_reports { - my $self = shift; - my $es = $self->model->es; - my $index = $self->index->name; - my $ua = LWP::UserAgent->new; - my $db = $self->home->file(qw(var tmp cpantesters.db)); - log_info { "Mirroring " . $self->db }; - $ua->mirror( $self->db, "$db.bz2" ); + my $self = shift; + + my $es = $self->es; + + log_info { 'Mirroring ' . $self->db }; + my $db = $self->mirror_file; + + $self->ua->mirror( $self->db, "$db.bz2" ) unless $self->skip_download; + if ( -e $db && stat($db)->mtime >= stat("$db.bz2")->mtime ) { - log_info {"DB hasn't been modified"}; - return; + log_info {'DB hasn\'t been modified'}; + return unless $self->force_refresh; } - bunzip2 "$db.bz2" => "$db", AutoClose => 1; + bunzip2 "$db.bz2" => "$db", AutoClose => 1 if -e "$db.bz2"; - my $scroll = $es->scrolled_search( - index => $index, - type => 'release', - query => { match_all => {} }, - size => 500, - search_type => 'scan', - scroll => '5m', + my $scroll = $es->scroll_helper( + es_doc_path('release'), + size => '500', + body => { + sort => '_doc', + }, ); my %releases; while ( my $release = $scroll->next ) { my $data = $release->{_source}; - $releases{ join( "-", grep { defined } $data->{distribution}, $data->{version} ) } + + # XXX temporary hack. This may be masking issues with release + # versions. (Olaf) + my $version = $data->{version}; + $version =~ s{\Av}{} if $version; + + $releases{ + join( '-', grep {defined} $data->{distribution}, $version ) } = $data; } - log_info { "Opening database file at " . $db }; - my $dbh = DBI->connect( "dbi:SQLite:dbname=" . $db ); + log_info { 'Opening database file at ' . $db }; + + my $dbh = DBI->connect( 'dbi:SQLite:dbname=' . $db ); my $sth; - $sth = $dbh->prepare("SELECT * FROM release"); + $sth = $dbh->prepare('SELECT * FROM release'); $sth->execute; my @bulk; + while ( my $row_from_db = $sth->fetchrow_hashref ) { + + # The testers db seems to return q{} where we would expect a version of + # 0. + + my $version = $row_from_db->{version} || 0; + + # weblint++ gets a name of 'weblint' and a version of '++-1.15' from + # the testers db. Special case it for now. Maybe try and get the db + # fixed. + + $version =~ s{\+}{}g; + $version =~ s{\A-}{}; - while ( my $data = $sth->fetchrow_hashref ) { - my $release = join( "-", $data->{dist}, $data->{version} ); - next unless ( $release = $releases{$release} ); - my $bulk = 0; - for (qw(fail pass na unknown)) { - $bulk = 1 if ( $data->{$_} != ( $release->{tests}->{$_} || 0 ) ); + my $release = join( '-', $row_from_db->{dist}, $version ); + my $release_doc = $releases{$release}; + + # there's a cpantesters dist we haven't indexed + next unless ($release_doc); + + my $insert_ok = 0; + + my $tester_results = $release_doc->{tests}; + if ( !$tester_results ) { + $tester_results = {}; + $insert_ok = 1; } - next unless ($bulk); - $release->{tests} - = { map { $_ => $data->{$_} } qw(fail pass na unknown) }; - push( @bulk, $release ); - $self->bulk( \@bulk ) if ( @bulk > 100 ); - } - $self->bulk( \@bulk ); - log_info {"done"}; -} -sub bulk { - my ( $self, $bulk ) = @_; - my @bulk; - my $index = $self->index->name; - while ( my $data = shift @$bulk ) { - push( - @bulk, - { index => { - index => $index, - id => $data->{id}, - type => 'release', - data => $data - } + # maybe use Data::Compare instead + for my $condition (qw(fail pass na unknown)) { + last if $insert_ok; + if ( ( $tester_results->{$condition} || 0 ) + != $row_from_db->{$condition} ) + { + $insert_ok = 1; } - ); + } + + next unless ($insert_ok); + my %tests = map { $_ => $row_from_db->{$_} } qw(fail pass na unknown); + $self->_bulk->update( { + doc => { tests => \%tests }, + doc_as_upsert => true, + id => $release_doc->{id}, + } ); } - $self->es->bulk( \@bulk ); + $self->_bulk->flush; + log_info {'done'}; } +__PACKAGE__->meta->make_immutable; 1; =pod @@ -104,7 +171,7 @@ sub bulk { =head1 SYNOPSIS $ bin/metacpan cpantesters - + =head1 DESCRIPTION Index CPAN Testers test results. diff --git a/lib/MetaCPAN/Script/CPANTestersAPI.pm b/lib/MetaCPAN/Script/CPANTestersAPI.pm new file mode 100644 index 000000000..0c9e6ca50 --- /dev/null +++ b/lib/MetaCPAN/Script/CPANTestersAPI.pm @@ -0,0 +1,139 @@ +package MetaCPAN::Script::CPANTestersAPI; + +use strict; +use warnings; + +use Cpanel::JSON::XS qw( decode_json ); +use ElasticSearchX::Model::Document::Types qw( ESBulk ); +use Log::Contextual qw( :log :dlog ); +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Types::TypeTiny qw( Uri ); +use MetaCPAN::Util qw( true false ); +use Moose; + +with 'MetaCPAN::Role::Script', 'MooseX::Getopt::Dashes'; + +has url => ( + is => 'ro', + isa => Uri, + coerce => 1, + lazy => 1, + builder => '_build_url', +); + +sub _build_url { + my ($self) = @_; + $ENV{HARNESS_ACTIVE} + ? 'file:' + . $self->home->child('t/var/cpantesters-release-api-fake.json') + : 'http://api-3.cpantesters.org/v3/release'; +} + +has _bulk => ( + is => 'ro', + isa => ESBulk, + lazy => 1, + default => sub { + $_[0]->es->bulk_helper( + es_doc_path('release'), + max_count => 250, + timeout => '30m', + ); + }, +); + +sub run { + my $self = shift; + $self->index_reports; + $self->es->indices->refresh; +} + +sub index_reports { + my $self = shift; + + my $es = $self->es; + + log_info { 'Fetching ' . $self->url }; + + my $res; + eval { $res = $self->ua->get( $self->url ) }; + return unless $res and $res->code == 200; + + my $json = $res->decoded_content; + my $data = decode_json $json; + + my $scroll = $es->scroll_helper( + es_doc_path('release'), + size => '500', + body => { + sort => '_doc', + }, + ); + + # Create a cache of all releases (dist + version combos) + my %releases; + while ( my $release = $scroll->next ) { + my $data = $release->{_source}; + + # XXX temporary hack. This may be masking issues with release + # versions. (Olaf) + my $version = $data->{version}; + $version =~ s{\Av}{} if $version; + + $releases{ + join( '-', grep {defined} $data->{distribution}, $version ) } + = $data; + } + + for my $row (@$data) { + + # The testers db seems to return q{} where we would expect + # a version of 0. + my $version = $row->{version} || 0; + + # weblint++ gets a name of 'weblint' and a version of '++-1.15' + # from the testers db. Special case it for now. Maybe try and + # get the db fixed. + + $version =~ s{\+}{}g; + $version =~ s{\A-}{}; + + my $release = join( '-', $row->{dist}, $version ); + my $release_doc = $releases{$release}; + + # there's a cpantesters dist we haven't indexed + next unless $release_doc; + + # Check if we need to update this data + my $insert_ok = 0; + my $tester_results = $release_doc->{tests}; + if ( !$tester_results ) { + $tester_results = {}; + $insert_ok = 1; + } + + # maybe use Data::Compare instead + for my $condition (qw(fail pass na unknown)) { + last if $insert_ok; + if ( + ( $tester_results->{$condition} || 0 ) != $row->{$condition} ) + { + $insert_ok = 1; + } + } + + next unless $insert_ok; + + my %tests = map { $_ => $row->{$_} } qw(fail pass na unknown); + $self->_bulk->update( { + doc => { tests => \%tests }, + doc_as_upsert => true, + id => $release_doc->{id}, + } ); + } + + $self->_bulk->flush; + log_info {'done'}; +} + +1; diff --git a/lib/MetaCPAN/Script/CVE.pm b/lib/MetaCPAN/Script/CVE.pm new file mode 100644 index 000000000..2c22c58f0 --- /dev/null +++ b/lib/MetaCPAN/Script/CVE.pm @@ -0,0 +1,261 @@ +package MetaCPAN::Script::CVE; + +use Moose; +use namespace::autoclean; + +use Cpanel::JSON::XS qw( decode_json ); +use Log::Contextual qw( :log :dlog ); +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Types::TypeTiny qw( Bool Str Uri ); +use MetaCPAN::Util qw( hit_total numify_version true false ); +use Path::Tiny qw( path ); +use Ref::Util qw( is_arrayref ); + +with 'MetaCPAN::Role::Script', 'MooseX::Getopt'; + +has cve_url => ( + is => 'ro', + isa => Uri, + coerce => 1, + default => 'https://cpan-security.github.io/cpansa-feed/cpansa.json', +); + +has cve_dev_url => ( + is => 'ro', + isa => Uri, + coerce => 1, + default => 'https://cpan-security.github.io/cpansa-feed/cpansa_dev.json', +); + +has test => ( + is => 'ro', + isa => Bool, + default => 0, + documentation => 'Test mode (pulls smaller development data set)', +); + +has json_file => ( + is => 'ro', + isa => Str, + default => 0, + documentation => + 'Path to JSON file to be read instead of URL (for testing)', +); + +my %range_ops = qw(< lt <= lte > gt >= gte); + +my %valid_keys = map { $_ => 1 } qw< + affected_versions + cpansa_id + cves + description + distribution + references + releases + reported + severity + versions +>; + +sub run { + my $self = shift; + my $data = $self->retrieve_cve_data; + $self->index_cve_data($data); + return 1; +} + +sub index_cve_data { + my ( $self, $data ) = @_; + + my $bulk = $self->es->bulk_helper( es_doc_path('cve') ); + + log_info {'Updating the cve index'}; + + for my $dist ( sort keys %{$data} ) { + for my $cpansa ( @{ $data->{$dist} } ) { + if ( !$cpansa->{cpansa_id} ) { + log_warn { sprintf( "Dist '%s' missing cpansa_id", $dist ) }; + next; + } + + my @matches; + + if ( !is_arrayref( $cpansa->{affected_versions} ) ) { + log_debug { + sprintf( "Dist '%s' has non-array affected_versions %s", + $dist, $cpansa->{affected_versions} ) + }; + + # Temp - remove after fixed upstream + # (affected_versions will always be an array) + $cpansa->{affected_versions} + = [ $cpansa->{affected_versions} ]; + + # next; + } + + my @filters; + my @afv_filters; + + for my $afv ( @{ $cpansa->{affected_versions} } ) { + + # Temp - remove after fixed upstream + # (affected_versions will always be an array) + next unless $afv; + + my @rules = map {s/\(.*?\)//gr} split /,/, $afv; + + my @rule_filters; + + for my $rule (@rules) { + my ( $op, $num ) = $rule =~ /^([=<>]*)(.*)$/; + $num = numify_version($num); + + if ( !$op ) { + log_debug { + sprintf( + "Dist '%s' - affected_versions has no operator", + $dist ) + }; + + # Temp - remove after fixed upstream + # (affected_versions will always have an operator) + $op ||= '='; + } + + if ( exists $range_ops{$op} ) { + push @rule_filters, + +{ + range => { + version_numified => + { $range_ops{$op} => $num } + } + }; + } + else { + push @rule_filters, + +{ term => { version_numified => $num } }; + } + } + + # multiple rules (csv) in affected_version line -> AND + if ( @rule_filters == 1 ) { + push @afv_filters, @rule_filters; + } + elsif ( @rule_filters > 1 ) { + push @afv_filters, { bool => { must => \@rule_filters } }; + } + } + + # multiple elements in affected_version -> OR + if ( @afv_filters == 1 ) { + push @filters, @afv_filters; + } + elsif ( @afv_filters > 1 ) { + push @filters, { bool => { should => \@afv_filters } }; + } + + if (@filters) { + my $query = {}; + + my $releases = $self->es->search( + es_doc_path('release'), + body => { + query => { + bool => { + must => [ + { term => { distribution => $dist } }, + @filters, + ] + } + }, + _source => [ "version", "name", "author", ], + size => 2000, + }, + ); + + if ( hit_total($releases) ) { + ## no critic (ControlStructures::ProhibitMutatingListFunctions) + @matches = map { $_->[0] } + sort { $a->[1] <=> $b->[1] } + map { + [ + $_->{_source}, + numify_version( $_->{_source}{version} ) + ]; + } @{ $releases->{hits}{hits} }; + } + else { + log_debug { + sprintf( "Dist '%s' doesn't have matches.", $dist ) + }; + next; + } + } + + my $doc_data = { + distribution => $dist, + cpansa_id => $cpansa->{cpansa_id}, + affected_versions => $cpansa->{affected_versions}, + cves => $cpansa->{cves}, + description => $cpansa->{description}, + references => $cpansa->{references}, + reported => $cpansa->{reported}, + severity => $cpansa->{severity}, + versions => [ map { $_->{version} } @matches ], + releases => [ map {"$_->{author}/$_->{name}"} @matches ], + }; + + for my $k ( keys %{$doc_data} ) { + delete $doc_data->{$k} unless exists $valid_keys{$k}; + } + + $bulk->update( { + id => $cpansa->{cpansa_id}, + doc => $doc_data, + doc_as_upsert => true, + } ); + } + } + + $bulk->flush; +} + +sub retrieve_cve_data { + my $self = shift; + + return decode_json( path( $self->json_file )->slurp ) if $self->json_file; + + my $url = $self->test ? $self->cve_dev_url : $self->cve_url; + + log_info { 'Fetching data from ', $url }; + my $resp = $self->ua->get($url); + + $self->handle_error( $resp->status_line ) unless $resp->is_success; + + # clean up headers if .json.gz is served as gzip type + # rather than json encoded with gzip + if ( $resp->header('Content-Type') eq 'application/x-gzip' ) { + $resp->header( 'Content-Type' => 'application/json' ); + $resp->header( 'Content-Encoding' => 'gzip' ); + } + + return decode_json( $resp->decoded_content ); +} + +__PACKAGE__->meta->make_immutable; + +1; + +=pod + +=head1 SYNOPSIS + + # bin/metacpan cve [--test] [json_file] + +=head1 DESCRIPTION + +Retrieves the CPAN CVE data from its source and +updates our ES information. + +=cut diff --git a/lib/MetaCPAN/Script/Check.pm b/lib/MetaCPAN/Script/Check.pm index 379b92433..81b115ca0 100644 --- a/lib/MetaCPAN/Script/Check.pm +++ b/lib/MetaCPAN/Script/Check.pm @@ -1,53 +1,52 @@ package MetaCPAN::Script::Check; +use strict; +use warnings; + +use File::Spec::Functions qw( catfile ); +use Log::Contextual qw( :log ); use Moose; -with 'MooseX::Getopt'; -use Log::Contextual qw( :log ); -with 'MetaCPAN::Role::Common'; -use File::Spec::Functions qw(catfile); -use ElasticSearch; +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Types::TypeTiny qw( Bool Int Str ); +use MetaCPAN::Util qw( true false ); -=head1 SYNOPSIS - -Performs checks on the MetaCPAN data store to make sure an -author/module/distribution has been indexed correctly and has the -appropriate information. - -=cut +with 'MetaCPAN::Role::Script', 'MooseX::Getopt'; has modules => ( is => 'ro', - isa => 'Bool', + isa => Bool, default => 0, documentation => 'check CPAN packages against MetaCPAN', ); has module => ( is => 'ro', - isa => 'Str', + isa => Str, default => '', documentation => 'the name of the module you are checking', ); has max_errors => ( is => 'ro', - isa => 'Int', + isa => Int, default => 0, - documentation => 'the maximum number of errors to encounter before stopping', + documentation => + 'the maximum number of errors to encounter before stopping', ); has errors_only => ( is => 'ro', - isa => 'Bool', + isa => Bool, default => 0, documentation => 'just show errors', ); has error_count => ( - is => 'rw', - isa => 'Int', + is => 'ro', + isa => Int, default => 0, - traits => ['NoGetopt'] + traits => ['NoGetopt'], + writer => '_set_error_count', ); sub run { @@ -58,127 +57,192 @@ sub run { sub check_modules { my $self = shift; - my (undef, @args) = @{$self->extra_argv}; - my $packages_file = catfile($self->cpan, 'modules', '02packages.details.txt'); - my $target = $self->module; - my $es = $self->es; + my ( undef, @args ) = @{ $self->extra_argv }; + my $packages_file + = catfile( $self->cpan, 'modules', '02packages.details.txt' ); + my $target = $self->module; + my $es = $self->es; my $packages_fh; - if (-e $packages_file) { - open($packages_fh, '<', $packages_file) - or die "Could not open packages file $packages_file: $!"; - } else { - die "Can't find 02packages.details.txt "; + if ( -e $packages_file ) { + open( $packages_fh, '<', $packages_file ) + or die "Could not open packages file $packages_file: $!"; + } + else { + die q{Can't find 02packages.details.txt}; } my $modules_start = 0; - while (my $line = <$packages_fh>) { + while ( my $line = <$packages_fh> ) { last if $self->max_errors && $self->error_count >= $self->max_errors; chomp($line); if ($modules_start) { - my ($pkg, $ver, $dist) = split(/\s+/, $line); + my ( $pkg, $ver, $dist ) = split( /\s+/, $line ); my @releases; # we only care about packages if we aren't searching for a # particular module or if it matches - if (!$target || $pkg eq $target) { - # look up this module in ElasticSearch and see what we have on it + if ( !$target || $pkg eq $target ) { + + # look up this module in ElasticSearch and see what we have on it my $results = $es->search( - index => $self->index->name, - type => 'file', - size => 100, # shouldn't get more than this - fields => [ - qw(name release author distribution version authorized indexed maturity date) - ], - query => {match_all => {}}, - filter => { - and => [ - {term => {'module.name' => $pkg}}, - {term => {'authorized' => 'true'}}, - {term => {'maturity' => 'released'}}, - ], + es_doc_path('file'), + query => { + bool => { + must => [ + { term => { 'module.name' => $pkg } }, + { term => { 'authorized' => true } }, + { term => { 'maturity' => 'released' } }, + ], + }, + size => 100, # shouldn't get more than this + _source => [ qw( + name + release + author + distribution + version + authorized + indexed + maturity + date + ) ], }, ); - my @files = @{$results->{hits}->{hits}}; + my @files = @{ $results->{hits}->{hits} }; # now find the first latest releases for these files foreach my $file (@files) { my $release_results = $es->search( - index => $self->index->name, - type => 'release', - size => 1, - fields => [qw(name status authorized version id date)], - query => {match_all => {}}, - filter => { - and => [ - {term => {'name' => $file->{fields}->{release}}}, - {term => {'status' => 'latest'}}, - ], + es_doc_path('release'), + query => { + bool => { + must => [ + { + term => { + name => + $file->{_source}->{release} + } + }, + { term => { status => 'latest' } }, + ], + }, + size => 1, + _source => + [qw(name status authorized version id date)], }, ); - if ($release_results->{hits}->{hits}->[0]) { - push(@releases, $release_results->{hits}->{hits}->[0]); + if ( $release_results->{hits}->{hits}->[0] ) { + push( @releases, + $release_results->{hits}->{hits}->[0] ); } } - # if we didn't find the latest release, then look at all of the releases - # so we can find out what might be wrong - if (!@releases) { + # if we didn't find the latest release, then look at all of the + # releases so we can find out what might be wrong + if ( !@releases ) { foreach my $file (@files) { my $release_results = $es->search( - index => $self->index->name, - type => 'release', - size => 1, - fields => [qw(name status authorized version id date)], - query => {match_all => {}}, - filter => {and => [{term => {'name' => $file->{fields}->{release}}}]}, + es_doc_path('release'), + query => { + bool => { + must => [ + { + term => { + name => $file->{_source} + ->{release} + } + }, + ], + }, + size => 1, + _source => [ + qw(name status authorized version id date) + ], + }, ); - push(@releases, @{$release_results->{hits}->{hits}}); + push( @releases, + @{ $release_results->{hits}->{hits} } ); } } # if we found the releases tell them about it if (@releases) { - if (@releases == 1 && $releases[0]->{fields}->{status} eq 'latest') { - log_info { "Found latest release $releases[0]->{fields}->{name} for $pkg" } + if ( @releases == 1 + && $releases[0]->{_source}->{status} eq 'latest' ) + { + log_info { + "Found latest release $releases[0]->{_source}->{name} for $pkg"; + } unless $self->errors_only; - } else { - log_error { "Could not find latest release for $pkg" }; + } + else { + log_error {"Could not find latest release for $pkg"}; foreach my $rel (@releases) { - log_warn { " Found release $rel->{fields}->{name}" }; - log_warn { " STATUS : $rel->{fields}->{status}" }; - log_warn { " AUTORIZED : $rel->{fields}->{authorized}" }; - log_warn { " DATE : $rel->{fields}->{date}" }; + log_warn { + " Found release $rel->{_source}->{name}"; + }; + log_warn { + " STATUS : $rel->{_source}->{status}"; + }; + log_warn { + " AUTORIZED : $rel->{_source}->{authorized}"; + }; + log_warn { + " DATE : $rel->{_source}->{date}"; + }; } - $self->error_count($self->error_count + 1); + $self->_set_error_count( $self->error_count + 1 ); } - } elsif (@files) { - log_error { "Module $pkg doesn't have any releases in ElasticSearch!" }; + } + elsif (@files) { + log_error { + "Module $pkg doesn't have any releases in ElasticSearch!"; + }; foreach my $file (@files) { - log_warn { " Found file $file->{fields}->{name}" }; - log_warn { " RELEASE : $file->{fields}->{release}" }; - log_warn { " AUTHOR : $file->{fields}->{author}" }; - log_warn { " AUTHORIZED : $file->{fields}->{authorized}" }; - log_warn { " DATE : $file->{fields}->{date}" }; + log_warn {" Found file $file->{_source}->{name}"}; + log_warn { + " RELEASE : $file->{_source}->{release}"; + }; + log_warn { + " AUTHOR : $file->{_source}->{author}"; + }; + log_warn { + " AUTHORIZED : $file->{_source}->{authorized}"; + }; + log_warn {" DATE : $file->{_source}->{date}"}; } - $self->error_count($self->error_count + 1); - } else { - log_error { "Module $pkg [$dist] doesn't not appear in ElasticSearch!" }; - $self->error_count($self->error_count + 1); + $self->_set_error_count( $self->error_count + 1 ); + } + else { + log_error { + "Module $pkg [$dist] doesn't not appear in ElasticSearch!"; + }; + $self->_set_error_count( $self->error_count + 1 ); } last if $self->module; } - } elsif (!length $line) { + } + elsif ( !length $line ) { $modules_start = 1; } } } +__PACKAGE__->meta->make_immutable; 1; +=pod + +=head1 SYNOPSIS + +Performs checks on the MetaCPAN data store to make sure an +author/module/distribution has been indexed correctly and has the +appropriate information. + =head2 check_modules Checks all of the modules in CPAN against the information in ElasticSearch. diff --git a/lib/MetaCPAN/Script/Checksum.pm b/lib/MetaCPAN/Script/Checksum.pm new file mode 100644 index 000000000..57d1b68af --- /dev/null +++ b/lib/MetaCPAN/Script/Checksum.pm @@ -0,0 +1,137 @@ +package MetaCPAN::Script::Checksum; + +use Moose; + +use Log::Contextual qw( :log ); +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Types::TypeTiny qw( Bool Int ); +use MetaCPAN::Util qw( true false ); + +use Digest::file qw( digest_file_hex ); + +with 'MooseX::Getopt', 'MetaCPAN::Role::Script'; + +=head1 SYNOPSIS + +Fill checksums for releases + +=cut + +has limit => ( + is => 'ro', + isa => Int, + default => 1000, +); + +has dry_run => ( + is => 'ro', + isa => Bool, + default => 1, +); + +sub run { + my $self = shift; + + my $bulk; + if ( !$self->dry_run ) { + $bulk = $self->es->bulk_helper( es_doc_path('release') ); + } + else { + log_warn {"--- DRY-RUN ---"}; + } + + log_info {"Searching for releases missing checksums"}; + + my $scroll = $self->es->scroll_helper( + es_doc_path('release'), + scroll => '10m', + body => { + query => { + bool => { + must_not => [ + { + exists => { + field => "checksum_md5" + } + }, + ], + }, + }, + _source => [qw( name download_url )], + }, + ); + + log_warn { "Found " . $scroll->total . " releases" }; + log_warn { "Limit is " . $self->limit }; + + my $count = 0; + + while ( my $p = $scroll->next ) { + if ( $self->limit >= 0 and $count++ >= $self->limit ) { + log_info {"Max number of changes reached."}; + last; + } + + log_info { "Adding checksums for " . $p->{_source}{name} }; + + if ( my $download_url = $p->{_source}{download_url} ) { + my $file + = $self->cpan . "/authors" . $download_url =~ s/^.*authors//r; + my $checksum_md5 = digest_file_hex( $file, 'MD5' ); + my $checksum_sha256 = digest_file_hex( $file, 'SHA-256' ); + + if ( $self->dry_run ) { + log_info { "--- MD5: " . $checksum_md5 } + log_info { "--- SHA256: " . $checksum_sha256 } + } + else { + $bulk->update( { + id => $p->{_id}, + doc => { + checksum_md5 => $checksum_md5, + checksum_sha256 => $checksum_sha256 + }, + doc_as_upsert => true, + } ); + } + } + else { + log_info { + $p->{_source}{name} . " is missing a download_url" + }; + } + } + + if ( !$self->dry_run ) { + $bulk->flush; + } + + log_info {'Finished adding checksums'}; +} + +__PACKAGE__->meta->make_immutable; +1; + +=pod + +=head1 SYNOPSIS + + # bin/metacpan checksum --[no-]dry_run --limit X + +=head1 DESCRIPTION + +Backfill checksums for releases + +=head2 dry_run + +Don't update - just show what would have been updated (default) + +=head2 no-dry_run + +Update records + +=head2 limit + +Max number of records to update. default=1000, for unlimited set to -1 + +=cut diff --git a/lib/MetaCPAN/Script/Contributor.pm b/lib/MetaCPAN/Script/Contributor.pm new file mode 100644 index 000000000..230216060 --- /dev/null +++ b/lib/MetaCPAN/Script/Contributor.pm @@ -0,0 +1,100 @@ +package MetaCPAN::Script::Contributor; + +use strict; +use warnings; + +use Moose; + +use Log::Contextual qw( :log ); + +use MetaCPAN::Types::TypeTiny qw( Bool HashRef Int Str ); + +with 'MetaCPAN::Role::Script', 'MooseX::Getopt', + 'MetaCPAN::Script::Role::Contributor'; + +has all => ( + is => 'ro', + isa => Bool, + default => 0, + documentation => 'update contributors for *all* releases', +); + +has distribution => ( + is => 'ro', + isa => Str, + documentation => + 'update contributors for all releases matching distribution name', +); + +has release => ( + is => 'ro', + isa => Str, + documentation => + 'update contributors for a single release (format: author/release_name)', +); + +has age => ( + is => 'ro', + isa => Int, + documentation => 'update contributors for a given number of days back', +); + +has author_release => ( + is => 'ro', + isa => HashRef, + lazy => 1, + builder => '_build_author_release', +); + +sub _build_author_release { + my $self = shift; + return unless $self->release; + my ( $author, $release ) = split m{/}, $self->release; + $author && $release + or die + "Error: invalid 'release' argument (format: PAUSEID/DISTRIBUTION-VERSION)"; + return +{ + author => $author, + release => $release, + }; +} + +sub run { + my $self = shift; + + my $query + = $self->all ? { match_all => {} } + : $self->distribution + ? { term => { distribution => $self->distribution } } + : $self->release ? { + bool => { + must => [ + { term => { author => $self->author_release->{author} } }, + { term => { name => $self->author_release->{release} } }, + ] + } + } + : $self->age + ? { range => { date => { gte => sprintf( 'now-%dd', $self->age ) } } } + : return; + + $self->update_contributors($query); +} + +__PACKAGE__->meta->make_immutable; +1; + +__END__ + +=head1 SYNOPSIS + + # bin/metacpan contributor --all + # bin/metacpan contributor --distribution Moose + # bin/metacpan contributor --release ETHER/Moose-2.1806 + +=head1 DESCRIPTION + +Update the list of contributors (CPAN authors only) of all/matching +releases in the 'contributor' type (index). + +=cut diff --git a/lib/MetaCPAN/Script/Cover.pm b/lib/MetaCPAN/Script/Cover.pm new file mode 100644 index 000000000..e14f51d0c --- /dev/null +++ b/lib/MetaCPAN/Script/Cover.pm @@ -0,0 +1,138 @@ +package MetaCPAN::Script::Cover; + +use Moose; +use namespace::autoclean; + +use Cpanel::JSON::XS qw( decode_json ); +use Log::Contextual qw( :log :dlog ); +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Types::TypeTiny qw( Bool Str Uri ); +use Path::Tiny qw( path ); +use MetaCPAN::Util qw( hit_total true false ); + +with 'MetaCPAN::Role::Script', 'MooseX::Getopt'; + +has cover_url => ( + is => 'ro', + isa => Uri, + coerce => 1, + default => 'http://cpancover.com/latest/cpancover.json', +); + +has cover_dev_url => ( + is => 'ro', + isa => Uri, + coerce => 1, + default => 'http://cpancover.com/latest/cpancover_dev.json', +); + +has test => ( + is => 'ro', + isa => Bool, + default => 0, + documentation => 'Test mode (pulls smaller development data set)', +); + +has json_file => ( + is => 'ro', + isa => Str, + default => 0, + documentation => + 'Path to JSON file to be read instead of URL (for testing)', +); + +my %valid_keys + = map { $_ => 1 } qw< branch condition statement subroutine total >; + +sub run { + my $self = shift; + my $data = $self->retrieve_cover_data; + $self->index_cover_data($data); + return 1; +} + +sub index_cover_data { + my ( $self, $data ) = @_; + + my $bulk = $self->es->bulk_helper( es_doc_path('cover') ); + + log_info {'Updating the cover index'}; + + for my $dist ( sort keys %{$data} ) { + for my $version ( keys %{ $data->{$dist} } ) { + my $release = $dist . '-' . $version; + my $rel_check = $self->es->search( + es_doc_path('release'), + size => 0, + body => { + query => { term => { name => $release } }, + }, + ); + if ( hit_total($rel_check) ) { + log_info { "Adding release info for '" . $release . "'" }; + } + else { + log_warn { "Release '" . $release . "' does not exist." }; + next; + } + + my %doc_data = %{ $data->{$dist}{$version}{coverage}{total} }; + + for my $k ( keys %doc_data ) { + delete $doc_data{$k} unless exists $valid_keys{$k}; + } + + $bulk->update( { + id => $release, + doc => { + distribution => $dist, + version => $version, + release => $release, + criteria => \%doc_data, + }, + doc_as_upsert => true, + } ); + } + } + + $bulk->flush; +} + +sub retrieve_cover_data { + my $self = shift; + + return decode_json( path( $self->json_file )->slurp ) if $self->json_file; + + my $url = $self->test ? $self->cover_dev_url : $self->cover_url; + + log_info { 'Fetching data from ', $url }; + my $resp = $self->ua->get($url); + + $self->handle_error( $resp->status_line ) unless $resp->is_success; + + # clean up headers if .json.gz is served as gzip type + # rather than json encoded with gzip + if ( $resp->header('Content-Type') eq 'application/x-gzip' ) { + $resp->header( 'Content-Type' => 'application/json' ); + $resp->header( 'Content-Encoding' => 'gzip' ); + } + + return decode_json( $resp->decoded_content ); +} + +__PACKAGE__->meta->make_immutable; + +1; + +=pod + +=head1 SYNOPSIS + + # bin/metacpan cover [--test] + +=head1 DESCRIPTION + +Retrieves the CPAN cover data from its source and +updates our ES information. + +=cut diff --git a/lib/MetaCPAN/Script/External.pm b/lib/MetaCPAN/Script/External.pm new file mode 100644 index 000000000..76d50ee59 --- /dev/null +++ b/lib/MetaCPAN/Script/External.pm @@ -0,0 +1,134 @@ +package MetaCPAN::Script::External; + +use Moose; +use namespace::autoclean; + +use Email::Sender::Simple (); +use Email::Simple (); +use Log::Contextual qw( :log ); +use MetaCPAN::ESConfig qw( es_doc_path ); + +use MetaCPAN::Types::TypeTiny qw( Str ); +use MetaCPAN::Util qw( true false ); + +with( + 'MetaCPAN::Role::Script', + 'MetaCPAN::Script::Role::External::Cygwin', + 'MetaCPAN::Script::Role::External::Debian', + 'MooseX::Getopt', +); + +has external_source => ( + is => 'ro', + isa => Str, + required => 1, +); + +has email_to => ( + is => 'ro', + isa => Str, + required => 1, +); + +sub run { + my $self = shift; + my $ret; + + $ret = $self->run_cygwin if $self->external_source eq 'cygwin'; + $ret = $self->run_debian if $self->external_source eq 'debian'; + + my $email_body = $ret->{errors_email_body}; + if ($email_body) { + my $email = Email::Simple->create( + header => [ + 'Content-Type' => 'text/plain; charset=utf-8', + To => $self->email_to, + From => 'noreply@metacpan.org', + Subject => 'Package mapping failures report for ' + . $self->external_source, + 'MIME-Version' => '1.0', + ], + body => $email_body, + ); + Email::Sender::Simple->send($email); + + log_debug { "Sending email to " . $self->email_to . ":" }; + log_debug {"Email body:"}; + log_debug {$email_body}; + } + + $self->update( $ret->{dist} ); +} + +sub update { + my ( $self, $dist ) = @_; + my $external_source = $self->external_source; + + my $scroll = $self->es->scroll_helper( + es_doc_path('distribution'), + scroll => '10m', + body => { + query => { + exists => { field => "external_package." . $external_source } + } + }, + ); + + my @to_remove; + + while ( my $s = $scroll->next ) { + my $name = $s->{_source}{name}; + next unless $name; + + if ( exists $dist->{$name} ) { + delete $dist->{$name} + if $dist->{$name} eq + $s->{_source}{external_package}{$external_source}; + } + else { + push @to_remove => $name; + } + } + + my $bulk = $self->es->bulk_helper( es_doc_path('distribution'), ); + + for my $d ( keys %{$dist} ) { + log_debug {"[$external_source] adding $d"}; + $bulk->update( { + id => $d, + doc => +{ + 'external_package' => { + $external_source => $dist->{$d} + } + }, + doc_as_upsert => true, + } ); + } + + for my $d (@to_remove) { + log_debug {"[$external_source] removing $d"}; + $bulk->update( { + id => $d, + doc => +{ + 'external_package' => { + $external_source => undef + } + } + } ); + } + + $bulk->flush; +} + +__PACKAGE__->meta->make_immutable; + +1; + +=pod + +=head1 SYNOPSIS + + # bin/metacpan external --external_source SOURCE + +=cut + diff --git a/lib/MetaCPAN/Script/Favorite.pm b/lib/MetaCPAN/Script/Favorite.pm new file mode 100644 index 000000000..958203e85 --- /dev/null +++ b/lib/MetaCPAN/Script/Favorite.pm @@ -0,0 +1,258 @@ +package MetaCPAN::Script::Favorite; + +use Moose; + +use Log::Contextual qw( :log ); + +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Types::TypeTiny qw( Bool Int Str ); +use MetaCPAN::Util qw( true false ); + +with 'MooseX::Getopt', 'MetaCPAN::Role::Script'; + +=head1 SYNOPSIS + +Updates the dist_fav_count field in 'file' by the count of ++ in 'favorite' + +=cut + +has queue => ( + is => 'ro', + isa => Bool, + default => 0, + documentation => 'Use the queue for updates', +); + +has check_missing => ( + is => 'ro', + isa => Bool, + default => 0, + documentation => + 'Report distributions that are missing from "file" or queue jobs if "--queue" specified', +); + +has age => ( + is => 'ro', + isa => Int, + documentation => + 'Update distributions that were voted on in the last X minutes', +); + +has limit => ( + is => 'ro', + isa => Int, + documentation => 'Limit number of results', +); + +has distribution => ( + is => 'ro', + isa => Str, + documentation => 'Update only a given distribution', +); + +has count => ( + is => 'ro', + isa => Int, + documentation => + 'Update this count to a given distribution (will only work with "--distribution"', +); + +sub run { + my $self = shift; + + if ( $self->count and !$self->distribution ) { + die + "Cannot set count in a distribution search mode, this flag only applies to a single distribution. please use together with --distribution DIST"; + } + + if ( $self->check_missing and $self->distribution ) { + die + "check_missing doesn't work in filtered mode - please remove other flags"; + } + + $self->index_favorites; + $self->es->indices->refresh; +} + +sub index_favorites { + my $self = shift; + + my $query = { match_all => {} }; + my $age_filter; + if ( $self->age ) { + $age_filter = { + range => { + date => { gte => sprintf( 'now-%dm', $self->age ) } + } + }; + } + + if ( $self->distribution ) { + $query = { term => { distribution => $self->distribution } }; + + } + elsif ( $self->age ) { + my $favs = $self->es->scroll_helper( + es_doc_path('favorite'), + scroll => '5m', + body => { + query => $age_filter, + _source => [qw< distribution >], + size => $self->limit || 500, + sort => '_doc', + } + ); + + my %recent_dists; + + while ( my $fav = $favs->next ) { + my $dist = $fav->{_source}{distribution}; + $recent_dists{$dist}++ if $dist; + } + + my @keys = keys %recent_dists; + if (@keys) { + $query = { terms => { distribution => \@keys } }; + } + } + + # get total fav counts for distributions + + my %dist_fav_count; + + if ( $self->count ) { + $dist_fav_count{ $self->distribution } = $self->count; + } + else { + my $favs = $self->es->scroll_helper( + es_doc_path('favorite'), + scroll => '30s', + body => { + query => $query, + _source => [qw< distribution >], + size => 500, + sort => '_doc', + }, + ); + + while ( my $fav = $favs->next ) { + my $dist = $fav->{_source}{distribution}; + $dist_fav_count{$dist}++ if $dist; + } + + log_debug {"Done counting favs for distributions"}; + } + + # Report missing distributions if requested + + if ( $self->check_missing ) { + my %missing; + my @age_filter; + if ( $self->age ) { + @age_filter = ( must => [$age_filter] ); + } + + my $files = $self->es->scroll_helper( + es_doc_path('file'), + scroll => '15m', + body => { + query => { + bool => { + must_not => [ + { range => { dist_fav_count => { gte => 1 } } } + ], + @age_filter, + } + }, + _source => [qw< distribution >], + size => 500, + sort => '_doc', + }, + ); + + while ( my $file = $files->next ) { + my $dist = $file->{_source}{distribution}; + next unless $dist; + next if exists $missing{$dist} or exists $dist_fav_count{$dist}; + + if ( $self->queue ) { + log_debug {"Queueing: $dist"}; + + my @count_flag; + if ( $self->count or $dist_fav_count{$dist} ) { + @count_flag = ( + '--count', $self->count || $dist_fav_count{$dist} + ); + } + + $self->_add_to_queue( index_favorite => + [ '--distribution', $dist, @count_flag ] => + { priority => 0, attempts => 10 } ); + } + else { + log_debug {"Found missing: $dist"}; + } + + $missing{$dist} = 1; + last if $self->limit and scalar( keys %missing ) >= $self->limit; + } + + my $total_missing = scalar( keys %missing ); + log_debug {"Total missing: $total_missing"} unless $self->queue; + + return; + } + + # Update fav counts for files per distributions + + for my $dist ( keys %dist_fav_count ) { + log_debug {"Dist $dist"}; + + if ( $self->queue ) { + $self->_add_to_queue( + index_favorite => [ + '--distribution', + $dist, + '--count', + ( $self->count ? $self->count : $dist_fav_count{$dist} ) + ] => { priority => 0, attempts => 10 } + ); + + } + else { + my $bulk = $self->es->bulk_helper( + es_doc_path('file'), + max_count => 250, + timeout => '120m', + ); + + my $files = $self->es->scroll_helper( + es_doc_path('file'), + scroll => '15s', + body => { + query => { term => { distribution => $dist } }, + _source => false, + size => 500, + sort => '_doc', + }, + ); + + while ( my $file = $files->next ) { + my $id = $file->{_id}; + my $cnt = $dist_fav_count{$dist}; + + log_debug {"Updating file id $id with fav_count $cnt"}; + + $bulk->update( { + id => $file->{_id}, + doc => { dist_fav_count => $cnt }, + } ); + } + + $bulk->flush; + } + } +} + +__PACKAGE__->meta->make_immutable; +1; diff --git a/lib/MetaCPAN/Script/First.pm b/lib/MetaCPAN/Script/First.pm new file mode 100644 index 000000000..71b2c3a79 --- /dev/null +++ b/lib/MetaCPAN/Script/First.pm @@ -0,0 +1,75 @@ +package MetaCPAN::Script::First; + +use strict; +use warnings; + +use Log::Contextual qw( :log ); +use Moose; +use MetaCPAN::Types::TypeTiny qw( Str ); + +with 'MetaCPAN::Role::Script', 'MooseX::Getopt'; + +has distribution => ( + is => 'ro', + isa => Str, + documentation => q{set the 'first' for only this distribution}, +); + +sub run { + my $self = shift; + my $distributions = $self->model->doc("distribution"); + $distributions + = $distributions->query( { term => { name => $self->distribution } } ) + if $self->distribution; + $distributions = $distributions->size(500)->scroll; + + log_info { "processing " . $distributions->total . " distributions" }; + + while ( my $distribution = $distributions->next ) { + my $release = $distribution->set_first_release; + $release + ? log_debug { + "@{[ $release->name ]} by @{[ $release->author ]} was first"; + } + : log_warn { + "no release found for distribution @{[$distribution->name]}"; + }; + } + + # Everything changed - reboot the world! + $self->cdn_purge_all; + + 1; +} + +__PACKAGE__->meta->make_immutable; +1; + +=pod + +=head1 NAME + +MetaCPAN::Script::First - Set the C bit after a full reindex + +=head1 SYNOPSIS + + $ bin/metacpan first --level debug + $ bin/metacpan first --distribution Moose + +=head1 DESCRIPTION + +Setting the L bit cannot be +set when indexing archives in parallel, e.g. when doing a full reindex. +This script sets the C bit once all archives have been indexed. + +See L for more +information. + +=head1 OPTIONS + +=head2 distribution + +Only set the L property for releases of this distribution. + +=cut + diff --git a/lib/MetaCPAN/Script/Latest.pm b/lib/MetaCPAN/Script/Latest.pm index b4fd00a18..e678806fd 100644 --- a/lib/MetaCPAN/Script/Latest.pm +++ b/lib/MetaCPAN/Script/Latest.pm @@ -1,31 +1,74 @@ package MetaCPAN::Script::Latest; -use Moose; -use MooseX::Aliases; -with 'MooseX::Getopt'; +use strict; +use warnings; + use Log::Contextual qw( :log ); -with 'MetaCPAN::Role::Common'; -use Parse::CPAN::Packages::Fast; -use Time::Local; -use Regexp::Common qw(time); +use Moose; +use CPAN::DistnameInfo (); +use DateTime::Format::ISO8601 (); +use MetaCPAN::ESConfig qw( es_doc_path ); +use MetaCPAN::Types::TypeTiny qw( Bool Str ); +use MetaCPAN::Util qw( true false ); +use Parse::CPAN::Packages::Fast (); + +with 'MetaCPAN::Role::Script', 'MooseX::Getopt'; + +has dry_run => ( + is => 'ro', + isa => Bool, + default => 0, +); -has dry_run => ( is => 'ro', isa => 'Bool', default => 0 ); -has distribution => ( is => 'ro', isa => 'Str' ); -has packages => ( is => 'ro', lazy_build => 1, traits => ['NoGetopt'], ); +has distribution => ( + is => 'ro', + isa => Str, +); + +has packages => ( + is => 'ro', + lazy => 1, + builder => '_build_packages', + traits => ['NoGetopt'], +); + +has force => ( + is => 'ro', + isa => Bool, + default => 0, +); sub _build_packages { return Parse::CPAN::Packages::Fast->new( - shift->cpan->file(qw(modules 02packages.details.txt.gz)) ); + shift->cpan->child(qw(modules 02packages.details.txt.gz)) + ->stringify ); +} + +sub _queue_latest { + my $self = shift; + my $dist = shift || $self->distribution; + + log_info { "queueing " . $dist }; + $self->_add_to_queue( + index_latest => + [ ( $self->force ? '--force' : () ), '--distribution', $dist ], + { attempts => 3 } + ); } sub run { - my $self = shift; - my $modules = $self->index->type('file'); - log_info {"Dry run: updates will not be written to ES"} - if ( $self->dry_run ); + my $self = shift; + + if ( $self->dry_run ) { + log_info {'Dry run: updates will not be written to ES'}; + } + my $p = $self->packages; - $self->index->refresh; + $self->es->indices->refresh; + # If a distribution name is passed get all the package names + # from 02packages that match that distribution so we can limit + # the ES query to just those modules. my @filter; if ( my $distribution = $self->distribution ) { foreach my $package ( $p->packages ) { @@ -33,159 +76,257 @@ sub run { push( @filter, $package ) if ( $dist && $dist eq $distribution ); } - log_info { "$distribution consists of " . @filter . " modules" }; + log_info { "$distribution consists of " . @filter . ' modules' }; } return if ( !@filter && $self->distribution ); - my $scroll = $modules->filter( - { and => [ - @filter - ? { or => [ - map { { term => { 'file.module.name' => $_ } } } - @filter - ] - } - : (), - { exists => { field => 'file.module.name' } }, - { term => { 'file.module.indexed' => \1 } }, - { term => { 'file.maturity' => 'released' } }, - { not => { filter => { term => { status => 'backpan' } } } }, - { not => { - filter => - { term => { 'file.distribution' => 'perl' } } - } - }, - ] + + # if we are just queueing a single distribution + if ( $self->queue and $self->distribution ) { + $self->_queue_latest(); + return; + } + + my %upgrade; + my %downgrade; + my %queued_distributions; + + my $total = @filter; + my $found_total = 0; + + my @module_filters; + if (@filter) { + while (@filter) { + my @modules = splice @filter, 0, 500; + + push @module_filters, + [ + { term => { 'module.indexed' => true } }, + { terms => { "module.name" => \@modules } }, + ]; } - )->fields( - [ 'file.module.name', 'file.author', - 'file.release', 'file.distribution', - 'file.date', 'file.status', - ] - )->size(10000)->raw->scroll('1h'); - - my ( %downgrade, %upgrade ); - log_debug { "Found " . $scroll->total . " modules" }; - - my $i = 0; - while ( my $file = $scroll->next ) { - $i++; - log_debug { "$i of " . $scroll->total } unless ( $i % 1000 ); - my $data = $file->{fields}; - my @modules - = ref $data->{'module.name'} - ? @{ $data->{'module.name'} } - : $data->{'module.name'}; - @modules = grep {defined} map { - eval { $p->package($_) } - } @modules; - foreach my $module (@modules) { - my $dist = $module->distribution; - if ( $dist->distvname eq $data->{release} - && $dist->cpanid eq $data->{author} ) - { - my $upgrade = $upgrade{ $data->{distribution} }; - next - if ( $upgrade - && $self->compare_dates( $upgrade->{date}, $data->{date} ) - ); - $upgrade{ $data->{distribution} } = $data; + } + else { + push @module_filters, + [ + { term => { 'module.indexed' => true } }, + { exists => { field => "module.name" } }, + ]; + } + for my $filter (@module_filters) { + + # This query will be used to produce a (scrolled) list of + # 'file' type records where the module.name matches the + # distribution name and which are released & + # indexed (the 'leading' module) + my $query = { + bool => { + must => [ + { + nested => { + path => 'module', + query => { bool => { must => $filter } } + } + }, + { term => { 'maturity' => 'released' } }, + ], + must_not => [ + { term => { status => 'backpan' } }, + { term => { distribution => 'perl' } } + ] } - elsif ( $data->{status} eq 'latest' ) { - $downgrade{ $data->{release} } = $data; + }; + + log_debug { + 'Searching for ' . @$filter . ' of ' . $total . ' modules' + } + if @module_filters > 1; + + my $scroll = $self->es->scroll_helper( { + es_doc_path('file'), + size => 100, + body => { + query => $query, + _source => [ + qw(author date distribution download_url module.name release status) + ], + sort => '_doc', + }, + } ); + + $found_total += $scroll->total; + + log_debug { 'Found ' . $scroll->total . ' modules' }; + log_debug { 'Found ' . $found_total . 'total modules' } + if @$filter != $total and $filter == $module_filters[-1]; + + my $i = 0; + + # For each file... + while ( my $file = $scroll->next ) { + $i++; + log_debug { "$i of " . $scroll->total } unless ( $i % 100 ); + my $file_data = $file->{_source}; + + # Convert module name into Parse::CPAN::Packages::Fast::Package object. + my @modules = grep {defined} + map { + eval { $p->package( $_->{name} ) } + } @{ $file_data->{module} }; + + $file_data->{date} + = DateTime::Format::ISO8601->parse_datetime( + $file_data->{date} ); + + # For each of the packages in this file... + foreach my $module (@modules) { + + # Get P:C:P:F:Distribution (CPAN::DistnameInfo) object for package. + my $dist = $module->distribution; + + if ( $self->queue ) { + my $d = $dist->dist; + $self->_queue_latest($d) + unless exists $queued_distributions{$d}; + $queued_distributions{$d} = 1; + next; + } + + # If 02packages has the same author/release for this package... + + # NOTE: CPAN::DistnameInfo doesn't parse some weird uploads + # (like /\.pm\.gz$/) so distvname might not be present. + # I assume cpanid always will be. + if ( defined( $dist->distvname ) + && $dist->distvname eq $file_data->{release} + && $dist->cpanid eq $file_data->{author} ) + { + my $upgrade = $upgrade{ $file_data->{distribution} }; + + # If multiple versions of a dist appear in 02packages + # only mark the most recent upload as latest. + next + if $upgrade && $upgrade->{date} > $file_data->{date}; + $upgrade{ $file_data->{distribution} } = $file_data; + } + elsif ( $file_data->{status} eq 'latest' ) { + $downgrade{ $file_data->{release} } = $file_data; + } } } } - while ( my ( $dist, $data ) = each %upgrade ) { - next if ( $data->{status} eq 'latest' ); - $self->reindex( $data, 'latest' ); + + my $bulk = $self->es->bulk_helper( es_doc_path('file') ); + + my %to_purge; + + while ( my ( $dist, $file_data ) = each %upgrade ) { + + # Don't reindex if already marked as latest. + # This just means that it hasn't changed (query includes 'latest'). + next if ( !$self->force and $file_data->{status} eq 'latest' ); + + $to_purge{ $file_data->{download_url} } = 1; + + $self->reindex( $bulk, $file_data, 'latest' ); } - while ( my ( $release, $data ) = each %downgrade ) { + + while ( my ( $release, $file_data ) = each %downgrade ) { + + # Don't downgrade if this release version is also marked as latest. + # This could happen if a module is moved to a new dist + # but the old dist remains (with other packages). + # This could also include bug fixes in our indexer, PAUSE, etc. next - if ( $upgrade{ $data->{distribution} } - && $upgrade{ $data->{distribution} }->{release} eq - $data->{release} ); - $self->reindex( $data, 'cpan' ); + if ( !$self->force + && $upgrade{ $file_data->{distribution} } + && $upgrade{ $file_data->{distribution} }->{release} eq + $file_data->{release} ); + + $to_purge{ $file_data->{download_url} } = 1; + + $self->reindex( $bulk, $file_data, 'cpan' ); } - $self->index->refresh; + $bulk->flush; + $self->es->indices->refresh; + + # Call Fastly to purge + $self->purge_cpan_distnameinfos( [ + map CPAN::DistnameInfo->new($_), keys %to_purge ] ); } +# Update the status for the release and all the files. sub reindex { - my ( $self, $source, $status ) = @_; - my $es = $self->es; + my ( $self, $bulk, $source, $status ) = @_; - my $release = $self->index->type('release')->get( - { author => $source->{author}, - name => $source->{release}, - } - ); + # Update the status on the release. + my $releases = $self->es->search( { + es_doc_path('release'), + body => { + query => { + bool => { + must => [ + { term => { author => $source->{author} } }, + { term => { name => $source->{release} } }, + ], + }, + }, + }, + _source => false, + } ); + my $release = $releases->{hits}{hits}[0]{_id}; - $release->status($status); log_info { - $status eq 'latest' ? "Upgrading " : "Downgrading ", - "release ", $release->name || ''; + $status eq 'latest' ? 'Upgrading ' : 'Downgrading ', + 'release ', $source->{release}, "($release)"; }; - $release->put unless ( $self->dry_run ); - - my $scroll = $es->scrolled_search( - { index => $self->index->name, - type => 'file', - scroll => '5m', - size => 1000, - search_type => 'scan', - query => { - filtered => { - query => { match_all => {} }, - filter => { - and => [ - { term => - { 'file.release' => $source->{release} } - }, - { term => { 'file.author' => $source->{author} } - } - ] - } - } - } - } + + if ( !$self->dry_run ) { + $self->es->update( { + es_doc_path('release'), + id => $release, + body => { + doc => { + status => $status, + }, + }, + } ); + } + + # Get all the files for the release. + my $scroll = $self->es->scroll_helper( + es_doc_path('file'), + size => 100, + body => { + query => { + bool => { + must => [ + { term => { 'release' => $source->{release} } }, + { term => { 'author' => $source->{author} } }, + ], + }, + }, + _source => [ 'status', 'file' ], + sort => '_doc', + }, ); - my @bulk; while ( my $row = $scroll->next ) { my $source = $row->{_source}; log_trace { - $status eq 'latest' ? "Upgrading " : "Downgrading ", - "file ", $source->{name} || ''; + $status eq 'latest' ? 'Upgrading ' : 'Downgrading ', + 'file ', $source->{name} || q[]; }; - push( - @bulk, - { index => { - index => $self->index->name, - type => 'file', - id => $row->{_id}, - data => { %$source, status => $status } - } - } - ) unless ( $self->dry_run ); - if ( @bulk > 100 ) { - $self->es->bulk( \@bulk ); - @bulk = (); - } - } - $self->es->bulk( \@bulk ) if (@bulk); -} -sub compare_dates { - my ( $self, $d1, $d2 ) = @_; - for ( $d1, $d2 ) { - if ( $_ =~ /$RE{time}{iso}{-keep}/ ) { - $_ = timelocal( $7, $6, $5, $4, $3 - 1, $2 ); - } + # Use bulk update to overwrite the status for X files at a time. + $bulk->update( { id => $row->{_id}, doc => { status => $status } } ) + unless $self->dry_run; } - return $d1 > $d2; + } __PACKAGE__->meta->make_immutable; +1; __END__ @@ -194,7 +335,7 @@ __END__ # bin/metacpan latest # bin/metacpan latest --dry_run - + =head1 DESCRIPTION After importing releases from cpan, this script will set the status diff --git a/lib/MetaCPAN/Script/Mapping.pm b/lib/MetaCPAN/Script/Mapping.pm index 7b84d2dbd..128c78b76 100644 --- a/lib/MetaCPAN/Script/Mapping.pm +++ b/lib/MetaCPAN/Script/Mapping.pm @@ -1,78 +1,764 @@ package MetaCPAN::Script::Mapping; use Moose; -with 'MooseX::Getopt'; -use Log::Contextual qw( :log ); -with 'MetaCPAN::Role::Common'; -has delete => ( is => 'ro', isa => 'Bool', default => 0, documentation => 'delete index if it exists already' ); +use Cpanel::JSON::XS (); +use Log::Contextual qw( :log ); +use MetaCPAN::ESConfig qw( es_config ); +use MetaCPAN::Types::TypeTiny qw( Bool HashRef Int ); +use Time::HiRes qw( sleep time ); + +use constant { + EXPECTED => 1, + NOT_EXPECTED => 0, +}; + +with 'MetaCPAN::Role::Script', 'MooseX::Getopt'; + +has arg_deploy_mapping => ( + init_arg => 'delete', + is => 'ro', + isa => Bool, + default => 0, + documentation => 'delete index if it exists already', +); + +has arg_delete_all => ( + init_arg => 'all', + is => 'ro', + isa => Bool, + default => 0, + documentation => + 'delete ALL existing indices (only effective in combination with "--delete")', +); + +has arg_verify_mapping => ( + init_arg => 'verify', + is => 'ro', + isa => Bool, + default => 0, + documentation => 'verify deployed index structure against definition', +); + +has arg_cluster_info => ( + init_arg => 'show_cluster_info', + is => 'ro', + isa => Bool, + default => 0, + documentation => 'show basic info about cluster and indices', +); + +has arg_await_timeout => ( + init_arg => 'await', + is => 'ro', + isa => Int, + default => 15, + documentation => + 'seconds before connection is considered failed with timeout', +); + +has cluster_info => ( + isa => HashRef, + traits => ['Hash'], + is => 'rw', + lazy => 1, + default => sub { {} }, +); + +has indices_info => ( + isa => HashRef, + traits => ['Hash'], + is => 'rw', + lazy => 1, + default => sub { {} }, +); sub run { my $self = shift; - log_info { "Putting mapping to ElasticSearch server" }; - $self->model->deploy( delete => $self->delete ); + + # Wait for the ElasticSearch Engine to become ready + if ( $self->await ) { + if ( $self->arg_deploy_mapping ) { + if ( $self->arg_delete_all ) { + $self->check_health; + $self->delete_all; + } + unless ( $self->deploy_mapping ) { + $self->print_error("Indices Re-creation has failed!"); + $self->exit_code(1); + } + } + + if ( $self->arg_verify_mapping ) { + $self->check_health; + unless ( $self->indices_valid( $self->_build_index_config ) ) { + $self->print_error("Indices Verification has failed!"); + $self->exit_code(1); + } + } + + if ( $self->arg_cluster_info ) { + $self->check_health; + $self->show_info; + } + } + +# The run() method is expected to communicate Success to the superior execution level + return ( $self->exit_code == 0 ? 1 : 0 ); +} + +sub delete_all { + my $self = $_[0]; + my $runtime_environment = 'production'; + + $runtime_environment = $ENV{'PLACK_ENV'} + if ( defined $ENV{'PLACK_ENV'} ); + $runtime_environment = $ENV{'MOJO_MODE'} + if ( defined $ENV{'MOJO_MODE'} ); + + my $is_development + = $ENV{HARNESS_ACTIVE} + || $runtime_environment eq 'development' + || $runtime_environment eq 'testing'; + + if ($is_development) { + foreach my $name ( grep !/\A\./, keys %{ $self->indices_info } ) { + $self->_delete_index($name); + } + } + else { + #Set System Error: 1 - EPERM - Operation not permitted + $self->exit_code(1); + $self->print_error("Operation not permitted!"); + $self->handle_error( + "Operation not permitted in environment: $runtime_environment", + 1 ); + } +} + +sub _delete_index { + my ( $self, $name ) = @_; + + log_info {"Deleting index: $name"}; + my $idx = $self->es->indices; + $idx->delete( index => $name ); + + my $exists; + my $end = time + 2; + while ( time < $end ) { + $exists = $idx->exists( index => $name ) or last; + sleep 0.1; + } + if ($exists) { + log_error {"Failed to delete index: $name"}; + } + return $exists; +} + +sub show_info { + my $self = $_[0]; + my $info_rs = { + 'cluster_info' => \%{ $self->cluster_info }, + 'indices_info' => \%{ $self->indices_info }, + }; + log_info { Cpanel::JSON::XS->new->utf8->pretty->encode($info_rs) }; +} + +sub _build_index_config { + my $self = $_[0]; + my $docs = es_config->documents; + my $indices = {}; + my $api_version = $self->es->api_version; + for my $name ( sort keys %$docs ) { + my $doc = $docs->{$name}; + my $index = $doc->{index} + or die "no index defined for $name documents"; + die "$index specified for multiple documents" + if $indices->{$index}; + my $mapping = es_config->mapping( $name, $api_version ); + my $settings = es_config->index_settings( $name, $api_version ); + if ( $api_version le '6_0' ) { + my $type = $doc->{type} + or die "no type defined for $name documents"; + $mapping = { $type => $mapping }; + } + $indices->{$index} = { + settings => $settings, + mappings => $mapping, + }; + } + + return $indices; +} + +sub deploy_mapping { + my $self = shift; + + $self->are_you_sure( + 'this will delete EVERYTHING and re-create the (empty) indexes'); + + # Deserialize the Index Mapping Structure + my $rindices = $self->_build_index_config; + + my $es = $self->es; + + # recreate the indices and apply the mapping + + for my $idx ( sort keys %$rindices ) { + $self->_delete_index($idx) + if $es->indices->exists( index => $idx ); + + log_info {"Creating index: $idx"}; + + $es->indices->create( index => $idx, body => $rindices->{$idx} ); + } + + $self->check_health(1); + + # done + log_info {"Done."}; + + return $self->indices_valid($rindices); } -sub map_perlmongers { - my ($self, $es) = @_; - return $es->put_mapping( - index => ['cpan'], - type => 'perlmongers', - properties => { - city => { type => "string" }, - continent => { type => "string" }, - email => { properties => { type => { type => "string" } } }, - inception_date => - { format => "dateOptionalTime", type => "date" }, - latitude => { type => "object" }, - location => { - properties => { - city => { type => "string" }, - continent => { type => "string" }, - country => { type => "string" }, - latitude => { type => "string" }, - longitude => { type => "string" }, - region => { type => "object" }, - state => { type => "string" }, - }, - }, - longitude => { type => "object" }, - mailing_list => { - properties => { - email => { - properties => { - domain => { type => "string" }, - type => { type => "string" }, - user => { type => "string" }, - }, - }, - name => { type => "string" }, - }, - }, - name => { type => "string" }, - pm_id => { type => "string" }, - region => { type => "string" }, - state => { type => "object" }, - status => { type => "string" }, - tsar => { - properties => { - email => { - properties => { - domain => { type => "string" }, - type => { type => "string" }, - user => { type => "string" }, - }, - }, - name => { type => "string" }, - }, - }, - web => { type => "string" }, - }, - - ); +sub _compare_mapping { + my ( $self, $sname, $rdeploy, $rmodel ) = @_; + my $imatch = 0; + + if ( defined $rdeploy && defined $rmodel ) { + my $json_parser = Cpanel::JSON::XS->new->allow_nonref; + my ( $deploy_type, $deploy_value ); + my ( $model_type, $model_value ); + + $imatch = 1; + + if ( ref $rdeploy eq 'HASH' ) { + foreach my $sfield ( sort keys %$rdeploy ) { + if ( defined $rdeploy->{$sfield} + && defined $rmodel->{$sfield} ) + { + $deploy_type = ref( $rdeploy->{$sfield} ); + $model_type = ref( $rmodel->{$sfield} ); + $deploy_value = $rdeploy->{$sfield}; + $model_value = $rmodel->{$sfield}; + + if ( $deploy_type eq 'JSON::PP::Boolean' ) { + $deploy_type = ''; + $deploy_value + = $json_parser->encode( $rdeploy->{$sfield} ); + } + + if ( $model_type eq 'JSON::PP::Boolean' ) { + $model_type = ''; + $model_value + = $json_parser->encode( $rmodel->{$sfield} ); + } + + if ( $deploy_type ne '' ) { + if ( $deploy_type eq 'HASH' + || $deploy_type eq 'ARRAY' ) + { + $imatch = ( + $imatch && $self->_compare_mapping( + $sname . '.' . $sfield, $deploy_value, + $model_value + ) + ); + } + else { # No Hash nor Array + if ( ${$deploy_value} ne ${$model_value} ) { + log_error { + 'Mismatch field: ' + . $sname . '.' + . $sfield . ' (' + . ${$deploy_value} . ' <> ' + . ${$model_value} . ')' + }; + $imatch = 0; + } + } + } + else { # Scalar Value + if ( + $sfield eq 'type' + && $model_value eq 'string' + && ( $deploy_value eq 'text' + || $deploy_value eq 'keyword' ) + ) + { + # ES5 automatically converts string types to text + # or keyword. once we upgrade to ES5 and update + # our mappings, this special case can be removed. + } + elsif ($sfield eq 'index' + && $model_value eq 'no' + && $deploy_value eq 'false' ) + { + # another ES5 string automatic conversion + } + elsif ( $deploy_value ne $model_value ) { + log_error { + 'Mismatch field: ' + . $sname . '.' + . $sfield . ' (' + . $deploy_value . ' <> ' + . $model_value . ')' + }; + $imatch = 0; + } + } + } + else { + unless ( defined $rdeploy->{$sfield} ) { + log_error { + 'Missing field: ' . $sname . '.' . $sfield + }; + $imatch = 0; + + } + unless ( defined $rmodel->{$sfield} ) { + if ( $sfield eq 'payloads' + && $rmodel->{type} + && $rmodel->{type} eq 'completion' + && !$rdeploy->{$sfield} ) + { + # ES5 doesn't allow payloads option. we've removed + # it from our mapping. but it gets a default + # value. ignore the default. + } + else { + log_error { + 'Missing definition: ' . $sname . '.' + . $sfield + }; + $imatch = 0; + } + } + } + } + } + elsif ( ref $rdeploy eq 'ARRAY' ) { + foreach my $iindex (@$rdeploy) { + if ( defined $rdeploy->[$iindex] + && defined $rmodel->[$iindex] ) + { + $deploy_type = ref( $rdeploy->[$iindex] ); + $model_type = ref( $rmodel->[$iindex] ); + $deploy_value = $rdeploy->[$iindex]; + $model_value = $rmodel->[$iindex]; + + if ( $deploy_type eq 'JSON::PP::Boolean' ) { + $deploy_type = ''; + $deploy_value + = $json_parser->encode( $rdeploy->[$iindex] ); + } + + if ( $model_type eq 'JSON::PP::Boolean' ) { + $model_type = ''; + $model_value + = $json_parser->encode( $rmodel->[$iindex] ); + } + + if ( $deploy_type eq '' ) { # Reference Value + if ( $deploy_type eq 'HASH' + || $deploy_type eq 'ARRAY' ) + { + $imatch = ( + $imatch && $self->_compare_mapping( + $sname . '[' . $iindex . ']', + $deploy_value, + $model_value + ) + ); + } + else { # No Hash nor Array + if ( ${$deploy_value} ne ${$model_value} ) { + log_error { + 'Mismatch field: ' + . $sname . '[' + . $iindex . '] (' + . ${$deploy_value} . ' <> ' + . ${$model_value} . ')' + }; + $imatch = 0; + } + } + } + else { # Scalar Value + if ( $deploy_value ne $model_value ) { + log_error { + 'Mismatch field: ' + . $sname . '[' + . $iindex . '] (' + . $deploy_value . ' <> ' + . $model_value . ')' + }; + $imatch = 0; + } + } + } + else { # Missing Field + unless ( defined $rdeploy->[$iindex] ) { + log_error { + 'Missing field: ' . $sname . '[' . $iindex . ']' + }; + $imatch = 0; + + } + unless ( defined $rmodel->[$iindex] ) { + log_error { + 'Missing definition: ' . $sname . '[' . $iindex + . ']' + }; + $imatch = 0; + } + } + } + } + } + else { # Missing Field + unless ( defined $rdeploy ) { + log_error { 'Missing field: ' . $sname }; + $imatch = 0; + } + unless ( defined $rmodel ) { + log_error { 'Missing definition: ' . $sname }; + $imatch = 0; + } + } + + if ( $self->{'logger'}->is_debug ) { + if ($imatch) { + log_debug {"field '$sname': ok"}; + } + else { + log_debug {"field '$sname': failed!"}; + } + } + + return $imatch; } +sub indices_valid { + my ( $self, $config_indices ) = @_; + my $valid = 0; + + if ( defined $config_indices && ref $config_indices eq 'HASH' ) { + my $deploy_indices = $self->es->indices->get_mapping; + $valid = 1; + + for my $idx ( sort keys %$config_indices ) { + my $config_mappings = $config_indices->{$idx} + && $config_indices->{$idx}->{'mappings'}; + my $deploy_mappings = $deploy_indices->{$idx} + && $deploy_indices->{$idx}->{'mappings'}; + if ( !$deploy_mappings ) { + log_error {"Missing index: $idx"}; + $valid = 0; + next; + } + + log_info { + "Verifying index: $idx" + }; + + if ( $self->_compare_mapping( + $idx, $deploy_mappings, $config_mappings + ) ) + { + log_info { + "Correct index: $idx (mapping deployed)" + }; + } + else { + log_error { + "Broken index: $idx (mapping does not match definition)" + }; + $valid = 0; + } + } + } + + if ($valid) { + log_info {"Verification indices: ok"}; + } + else { + log_info {"Verification indices: failed"}; + } + + return $valid; +} + +sub _get_indices_info { + my ( $self, $irefresh ) = @_; + + if ( $irefresh || scalar( keys %{ $self->indices_info } ) == 0 ) { + my $sinfo_rs = $self->es->cat->indices( h => [ 'index', 'health' ] ); + my $sindices_parsing = qr/^([^[:space:]]+) +([^[:space:]]+)/m; + + $self->indices_info( {} ); + + while ( $sinfo_rs =~ /$sindices_parsing/g ) { + $self->indices_info->{$1} + = { 'index_name' => $1, 'health' => $2 }; + } + } +} + +sub check_health { + my ( $self, $irefresh ) = @_; + my $ihealth = 0; + + $irefresh = 0 unless ( defined $irefresh ); + + $ihealth = $self->await; + + if ($ihealth) { + $self->_get_indices_info($irefresh); + + foreach ( keys %{ $self->indices_info } ) { + $ihealth = 0 + if ( $self->indices_info->{$_}->{'health'} eq 'red' ); + } + } + + return $ihealth; +} + +sub await { + my $self = $_[0]; + my $iready = 0; + + if ( scalar( keys %{ $self->cluster_info } ) == 0 ) { + my $es = $self->es; + my $iseconds = 0; + + log_info {"Awaiting Elasticsearch ..."}; + + do { + eval { + $iready = $es->ping; + + if ($iready) { + log_info { + "Awaiting $iseconds / " + . $self->arg_await_timeout + . " : ready" + }; + + $self->cluster_info( \%{ $es->info } ); + } + }; + + if ($@) { + if ( $iseconds < $self->arg_await_timeout ) { + log_info { + "Awaiting $iseconds / " + . $self->arg_await_timeout + . " : unavailable - sleeping ..." + }; + + sleep(1); + + $iseconds++; + } + else { + log_error { + "Awaiting $iseconds / " + . $self->arg_await_timeout + . " : unavailable - timeout!" + }; + + #Set System Error: 112 - EHOSTDOWN - Host is down + $self->exit_code(112); + $self->handle_error( $@, 1 ); + } + } + } while ( !$iready && $iseconds <= $self->arg_await_timeout ); + } + else { + #ElasticSearch Service is available + $iready = 1; + } + + return $iready; +} + +__PACKAGE__->meta->make_immutable; +1; + +__END__ + +=pod + +=head1 NAME + +MetaCPAN::Script::Mapping - Script to set the index and mapping the types + +=head1 SYNOPSIS + + # bin/metacpan mapping --show_cluster_info # show basic info about the cluster and indices + # bin/metacpan mapping --delete + # bin/metacpan mapping --delete --all # deletes ALL indices in the cluster + # bin/metacpan mapping --verify # compare deployed indices with project definitions + +=head1 DESCRIPTION + +This is the index mapping handling script. +Used rarely, but carries the most important task of setting +the index and mapping the types. + +=head1 OPTIONS + +This Script accepts the following options + +=over 4 + +=item Option C<--show_cluster_info> + +This option makes the Script show basic information about the I Cluster +and its indices. +This information has to be collected with the C Method. +On Script start-up it is empty. + + bin/metacpan mapping --show_cluster_info + +See L> + +=item Option C<--delete> + +This option makes the Script delete all indices configured in the project and re-create them emtpy. +It verifies the index integrity of the indices calling the methods +C and C. +If the C Method fails it will report an error. + + bin/metacpan mapping --delete + +B If the mapping deployment fails it exits the Script with B C< 1 >. + +See L> + +See L> + +See L> + +=item Option C<--all> + +This option is only effective in combination with Option C<--delete>. +It uses the information gathered by C to delete +B indices in the I Cluster. +This option is usefull to reconstruct a broken I Cluster + + bin/metacpan mapping --delete --all + +B It will throw an exceptions when not performed in an development or +testing environment. + +See L
}sg; + is( scalar(@err), 2, 'two parse errors listed ' ); + like( $err[0], qr/=head\b/, 'first error mentions =head' ); + like( $err[1], qr/C</, 'first error mentions C< ... >' ); +} done_testing; diff --git a/t/server/controller/rating.t b/t/server/controller/rating.t new file mode 100644 index 000000000..e59adb3c2 --- /dev/null +++ b/t/server/controller/rating.t @@ -0,0 +1,117 @@ +use strict; +use warnings; +use lib 't/lib'; + +use MetaCPAN::Server::Test qw( app GET POST test_psgi ); +use MetaCPAN::TestHelpers qw( decode_json_ok ); +use Test::More; + +test_psgi app, sub { + my $cb = shift; + + my $res; + + ok( $res = $cb->( GET '/rating/random-id' ), 'GET /rating/random-id' ); + + is $res->code, 404, 'not found'; + + ok( $res = $cb->( GET '/rating/_mapping' ), 'GET /rating/_mapping' ); + + is $res->code, 404, 'not found'; + + ok( $res = $cb->( GET '/rating/by_distributions?distribution=Moose' ), + 'GET /rating/by_distributions' ); + + is $res->code, 200, 'found'; + + my $ratings = decode_json_ok($res); + + is_deeply $ratings->{distributions}, {}, 'empty distributions'; + + ok( + $res = $cb->( + POST '/rating/_search', + Content => '{"query":{"term":{"distribution":"Moose"}}}' + ), + 'POST /rating/_search' + ); + + is $res->code, 200, 'found'; + + $ratings = decode_json_ok($res); + + is_deeply $ratings->{hits}{hits}, [], 'no hits'; + + ok( + $res = $cb->( + POST '/rating', + Content => '{"query":{"term":{"distribution":"Moose"}}}', + ), + 'POST /rating' + ); + + is $res->code, 200, 'found'; + + $ratings = decode_json_ok($res); + + is_deeply $ratings->{hits}{hits}, [], 'no hits'; + + ok( + $res = $cb->( + POST '/rating/_search?scroll=5m', + Content => '{"query":{"term":{"distribution":"Moose"}}}', + ), + 'POST /rating' + ); + + is $res->code, 200, 'found'; + + $ratings = decode_json_ok($res); + + is_deeply $ratings->{hits}{hits}, [], 'no hits'; + + is_deeply $ratings->{_scroll_id}, 'FAKE_SCROLL_ID', + 'gives fake scroll id'; + + ok( + $res + = $cb->( POST "/_search/scroll/$ratings->{_scroll_id}?scroll=5m", + ), + 'POST /_search/scroll/$id', + ); + + is $res->code, 200, 'found' + or diag $res->as_string; + + $ratings = decode_json_ok($res); + + is_deeply $ratings->{hits}{hits}, [], 'working with no hits'; + is $ratings->{_shards}{total}, 0, 'results are fake'; + + ok( + $res = $cb->( + POST '/rating/_search', + 'User-Agent' => 'MetaCPAN::Client/2.031001', + Content => '{"query":{"term":{"distribution":"Moose"}}}', + ), + 'POST /rating with MetaCPAN::Client test UA' + ); + + is $res->code, 200, 'found'; + + $ratings = decode_json_ok($res); + + is_deeply $ratings->{hits}{hits}, + [ + { + _source => { + distribution => 'Moose', + }, + }, + ], + 'no hits'; + +}; + +done_testing; + diff --git a/t/server/controller/release.t b/t/server/controller/release.t new file mode 100644 index 000000000..340382e46 --- /dev/null +++ b/t/server/controller/release.t @@ -0,0 +1,109 @@ +use strict; +use warnings; +use lib 't/lib'; + +use MetaCPAN::Server::Test qw( app GET test_psgi ); +use MetaCPAN::TestHelpers qw( decode_json_ok test_cache_headers ); +use Test::More; + +{ + no warnings 'redefine'; + + sub get_ok { + my ( $cb, $url, $desc, $headers ) = @_; + ok( my $res = $cb->( GET $url ), $desc || "GET $url" ); + is( $res->code, 200, 'code 200' ); + + test_cache_headers( $res, $headers ); + + return $res; + } +} + +sub get_json_ok { + return decode_json_ok( get_ok(@_) ); +} + +test_psgi app, sub { + my $cb = shift; + + # find (/release/DIST) + get_json_ok( + $cb, + '/release/Moose', + 'GET /release/dist', + { + # ??? + cache_control => 'private', + surrogate_key => + 'content_type=application/json content_type=application', + surrogate_control => undef, + } + ); + + # get (/release/AUTHOR/NAME) + get_json_ok( + $cb, + '/release/DOY/Moose-0.01', + 'GET /release/DOY/Moose-0.01', + { + # ??? + } + ); + + # versions (/release/versions/DIST) + my $versions = get_json_ok( + $cb, + '/release/versions/Moose', + 'GET /release/versions/Moose', + { + # ??? + } + ); + is( @{ $versions->{releases} }, 2, "Got 2 Moose versions (all)" ); + + # versions - specific (/release/versions/DIST?versions=VERSION) + my $versions_specific = get_json_ok( + $cb, + '/release/versions/Moose?versions=0.01', + 'GET /release/versions/Moose?versions=0.01', + { + # ??? + } + ); + is( @{ $versions_specific->{releases} }, + 1, "Got 1 Moose version (specificly requested)" ); + + # versions - latest (/release/versions/DIST?versions=latest) + my $versions_latest = get_json_ok( + $cb, + '/release/versions/Moose?versions=latest', + 'GET /release/versions/Moose?versions=latest', + { + # ??? + } + ); + is( @{ $versions_latest->{releases} }, + 1, "Got 1 Moose version (only latest requested)" ); + is( $versions_latest->{releases}[0]{status}, + 'latest', "Release status is latest" ); + + # versions - plain (/release/versions/DIST?plain=1) + ok( my $versions_plain = $cb->( GET '/release/versions/Moose?plain=1' ), + 'GET /release/versions/Moose?plain=1' ); + is( $versions_plain->code, 200, 'code 200' ); + ok( $versions_plain->content =~ /\A .+ \t .+ \n .+ \t .+ \z/xsm, + 'Content is plain text result' ); + + # latest_by_distribution (/release/latest_by_distribution/DIST) + get_json_ok( + $cb, + '/release/latest_by_distribution/Moose', + 'GET /release/latest_by_distribution/Moose', + { + # ??? + } + ); +}; + +done_testing; diff --git a/t/server/controller/root.t b/t/server/controller/root.t new file mode 100644 index 000000000..5039474a5 --- /dev/null +++ b/t/server/controller/root.t @@ -0,0 +1,19 @@ +use strict; +use warnings; +use lib 't/lib'; + +use MetaCPAN::Server::Test qw( app GET test_psgi ); +use Test::More; + +test_psgi app, sub { + my $cb = shift; + ok( my $res = $cb->( GET '/' ), "GET /" ); + is( $res->code, 302, 'got redirect' ); + is( + $res->header('Location'), + 'https://github.com/metacpan/metacpan-api/blob/master/docs/API-docs.md', + 'correct redirect target' + ); +}; + +done_testing; diff --git a/t/server/controller/scroll.t b/t/server/controller/scroll.t new file mode 100644 index 000000000..e9ba29ce1 --- /dev/null +++ b/t/server/controller/scroll.t @@ -0,0 +1,129 @@ +use strict; +use warnings; +use lib 't/lib'; + +use MetaCPAN::Server::Test qw( app GET POST test_psgi ); +use MetaCPAN::TestHelpers qw( decode_json_ok test_cache_headers ); +use Test::More; + +test_psgi app, sub { + my $cb = shift; + + test_scroll_methods($cb); + test_missing_scroll_id($cb); +}; + +sub test_missing_scroll_id { + my $cb = shift; + foreach my $req ( + [ scroll_url_param(), 'url param' ], + [ scroll_post_body(), 'post body' ], + [ scroll_query_string(), 'query string' ], + ) + { + is_deeply( + req_json( $cb, @$req, 500 ), + { message => 'Scroll Id required' }, + "error without scroll_id in $req->[-1]", + ); + } +} + +sub scroll_start { + return GET '/release/_search?size=1&scroll=5m'; +} + +sub scroll_url_param { + my $scroll_id = shift || q[]; + return GET "/_search/scroll/$scroll_id?scroll=5m"; +} + +sub scroll_post_body { + my $scroll_id = shift || q[]; + + # Use text/plain to avoid Catalyst trying to process the body. + return POST '/_search/scroll?scroll=5m', + Content_type => 'text/plain', + Content => $scroll_id; +} + +sub scroll_query_string { + my $scroll_id = shift || q[]; + return GET "/_search/scroll/?scroll_id=$scroll_id&scroll=5m"; +} + +sub req_json { + my ( $cb, $req, $desc, $code ) = @_; + ok( my $res = $cb->($req), $desc ); + + $code ||= 200; + is( $res->code, $code, "HTTP $code" ) + or diag Test::More::explain($res); + + test_cache_headers( + $res, + { + cache_control => 'private', + surrogate_key => + 'content_type=application/json content_type=application', + surrogate_control => undef, + } + ); + + my $json = decode_json_ok($res); + return $json; +} + +sub test_scroll_methods { + my $cb = shift; + + my $steps = [ + sub { + req_json( $cb, scroll_start(), 'start scroll' ); + }, + sub { + req_json( + $cb, + scroll_url_param( $_[0] ), + 'continue scroll with scroll_id in GET url' + ); + }, + sub { + req_json( + $cb, + scroll_post_body( $_[0] ), + 'continue scroll with scroll_id in POST body' + ); + }, + sub { + req_json( + $cb, + scroll_query_string( $_[0] ), + 'continue scroll with scroll_id in query string' + ); + }, + ]; + + my $scroll_id; + my @docs; + + # Repeat each type just to be sure. + foreach my $step ( shift(@$steps), (@$steps) x 2 ) { + + # Pass in previous scroll_id. + my $json = $step->($scroll_id); + + # Cache scroll_id for next call. + $scroll_id = $json->{_scroll_id}; + + # Save docs. + push @docs, $json->{hits}{hits}[0]; + note $docs[-1]->{_source}{name}; + } + + my %ids = map { ( $_->{_id} => $_ ) } @docs; + + is scalar( keys(%ids) ), scalar(@docs), 'got a new doc each time'; +} + +done_testing; diff --git a/t/server/controller/search/autocomplete.t b/t/server/controller/search/autocomplete.t new file mode 100644 index 000000000..760cd7405 --- /dev/null +++ b/t/server/controller/search/autocomplete.t @@ -0,0 +1,64 @@ +use strict; +use warnings; +use lib 't/lib'; + +use MetaCPAN::Server::Test qw( app GET test_psgi ); +use MetaCPAN::TestHelpers qw( decode_json_ok ); +use Test::More; + +test_psgi app, sub { + my $cb = shift; + + # test ES script using doc['blah'] value + { + ok( my $res = $cb->( GET '/search/autocomplete?q=Multiple::Modu' ), + 'GET' ); + my $json = decode_json_ok($res); + + my $got = [ map { $_->{fields}{documentation} } + @{ $json->{hits}{hits} } ]; + + is_deeply $got, [ qw( + Multiple::Modules + Multiple::Modules::A + Multiple::Modules::B + Multiple::Modules::RDeps + Multiple::Modules::Tester + Multiple::Modules::RDeps::A + Multiple::Modules::RDeps::Deprecated + ) ], + 'results are sorted lexically by module name + length' + or diag( Test::More::explain($got) ); + } +}; + +test_psgi app, sub { + my $cb = shift; + + # test ES script using doc['blah'] value + { + ok( + my $res + = $cb->( + GET '/search/autocomplete/suggest?q=Multiple::Modu' ), + 'GET' + ); + my $json = decode_json_ok($res); + + my $got = [ map $_->{name}, @{ $json->{suggestions} } ]; + + is_deeply $got, [ qw( + Multiple::Modules + Multiple::Modules::A + Multiple::Modules::B + Multiple::Modules::RDeps + Multiple::Modules::Tester + Multiple::Modules::RDeps::A + Multiple::Modules::RDeps::Deprecated + ) ], + 'results are sorted lexically by module name + length' + or diag( Test::More::explain($got) ); + } +}; + +done_testing; diff --git a/t/server/controller/search/reverse_dependencies.t b/t/server/controller/search/reverse_dependencies.t deleted file mode 100644 index 5583e8f53..000000000 --- a/t/server/controller/search/reverse_dependencies.t +++ /dev/null @@ -1,116 +0,0 @@ -use strict; -use warnings; -use Test::More; -use MetaCPAN::Server::Test; - -my %tests = ( - '/search/reverse_dependencies/NonExistent' => [ 404, [], [] ], - '/search/reverse_dependencies/Pod-Pm' => [ 200, [], [] ], - - # just dist name - '/search/reverse_dependencies/Multiple-Modules' => [ - 200, - [ qw( Multiple-Modules-RDeps-0.11 ) ], - [ qw( Multiple-Modules-RDeps-2.03 Multiple-Modules-RDeps-A-2.03 ) ], - ], - - # author/name-version - '/search/reverse_dependencies/LOCAL/Multiple-Modules-1.01' => [ - 200, - [ qw( Multiple-Modules-RDeps-0.11 ) ], - [ qw( Multiple-Modules-RDeps-2.03 Multiple-Modules-RDeps-A-2.03 ) ], - ], - - # older author/name-version with different modules - '/search/reverse_dependencies/LOCAL/Multiple-Modules-0.1' => [ - 200, - [ qw( Multiple-Modules-RDeps-0.11 ) ], - [ qw( Multiple-Modules-RDeps-2.03 Multiple-Modules-RDeps-Deprecated-0.01 ) ], - ], -); - -sub check_search_results { - my ($name, $res, $code, $rdeps) = @_; - ok( $res, $name ); - is( $res->code, $code, "code $code" ); - is( $res->header('content-type'), - 'application/json; charset=utf-8', - 'Content-type' - ); - ok( my $json = eval { decode_json( $res->content ) }, 'valid json' ); - return unless $code == 200; - - $json = $json->{hits}{hits} if $json->{hits}; - is scalar @$json, @$rdeps, 'got expected number of releases'; - is_deeply - [ sort map { join '-', @{$_->{_source}}{qw(distribution version)} } @$json ], - $rdeps, - 'got expected releases'; -} - -test_psgi app, sub { - my $cb = shift; - - # verify search results - while ( my ( $k, $v ) = each %tests ) { - my ( $code, $rdep_old, $rdep_latest ) = @$v; - - # all results - check_search_results("GET $k" => $cb->(GET $k - ), $code, [sort(@$rdep_old, @$rdep_latest)]); - - # only releases marked as latest - check_search_results("POST $k" => $cb->(POST $k, - Content => encode_json( - { query => { match_all => {} }, - filter => { - term => { - 'release.status' => 'latest' - }, - }, - } - ) - ), $code, [sort(@$rdep_latest)]); - } - - # test passing additional ES parameters - { - ok( my $res = $cb->( - POST "/search/reverse_dependencies/Multiple-Modules", - Content => encode_json( - { query => { match_all => {} }, size => 1 } - ) - ), - "POST" - ); - ok( my $json = eval { decode_json( $res->content ) }, 'valid json' ); - is( $json->{hits}->{total}, 3, 'total is 3' ); - is( scalar @{ $json->{hits}->{hits} }, 1, 'only 1 received' ); - } - - # test appending filters - { - ok( my $res = $cb->( - POST - "/search/reverse_dependencies/Multiple-Modules?fields=release.distribution", - Content => encode_json( - { query => { match_all => {} }, - filter => { - term => { - 'release.distribution' => - 'Multiple-Modules-RDeps-A' - }, - }, - } - ) - ), - "POST" - ); - ok( my $json = eval { decode_json( $res->content ) }, 'valid json' ); - is( $json->{hits}->{total}, 1, 'total is 1' ); - is( $json->{hits}->{hits}->[0]->{fields}->{distribution}, - 'Multiple-Modules-RDeps-A', 'filter worked' ); - } -}; - -done_testing; diff --git a/t/server/controller/source.t b/t/server/controller/source.t index 3622798b2..97d5f8cd1 100644 --- a/t/server/controller/source.t +++ b/t/server/controller/source.t @@ -1,39 +1,113 @@ - use strict; use warnings; +use lib 't/lib'; + +use Cpanel::JSON::XS (); +use MetaCPAN::Server::Test qw( app GET test_psgi ); +use MetaCPAN::TestHelpers qw( test_cache_headers ); use Test::More; -use MetaCPAN::Server::Test; my %tests = ( - '/source/DOESNEXIST' => 404, - '/source/DOY/Moose-0.01/' => 200, - '/source/DOY/Moose-0.01/MANIFEST' => 200, - '/source/DOY/Moose-0.01/MANIFEST?callback=foo' => 200, - '/source/DOY/Moose-0.01/Changes' => 200, - '/source/DOY/Moose-0.01/Changes?callback=foo' => 200, - '/source/Moose' => 200, + '/source/DOESNEXIST' => { + code => 404, + cache_control => 'private', + surrogate_key => + 'content_type=application/json content_type=application', + surrogate_control => undef + }, + '/source/DOY/Moose-0.01/' => { + code => 200, + cache_control => undef, + surrogate_key => + 'author=DOY content_type=text/html content_type=text', + surrogate_control => + 'max-age=31556952, stale-while-revalidate=86400, stale-if-error=2592000' + }, + '/source/DOY/Moose-0.01/Changes' => { + code => 200, + cache_control => undef, + surrogate_key => + 'author=DOY content_type=text/plain content_type=text', + surrogate_control => + 'max-age=31556952, stale-while-revalidate=86400, stale-if-error=2592000', + }, + '/source/DOY/Moose-0.01/Changes?callback=foo' => { + code => 200, + cache_control => undef, + surrogate_key => + 'author=DOY content_type=text/javascript content_type=text', + surrogate_control => + 'max-age=31556952, stale-while-revalidate=86400, stale-if-error=2592000', + }, + '/source/DOY/Moose-0.01/MANIFEST' => { + code => 200, + cache_control => undef, + surrogate_key => + 'author=DOY content_type=text/plain content_type=text', + surrogate_control => + 'max-age=31556952, stale-while-revalidate=86400, stale-if-error=2592000', + }, + '/source/DOY/Moose-0.01/MANIFEST?callback=foo' => { + code => 200, + cache_control => undef, + surrogate_key => + 'author=DOY content_type=text/javascript content_type=text', + surrogate_control => + 'max-age=31556952, stale-while-revalidate=86400, stale-if-error=2592000', + }, + '/source/Moose' => { + code => 200, + cache_control => 'private', + surrogate_key => + 'author=DOY content_type=text/plain content_type=text', + surrogate_control => undef + }, ); test_psgi app, sub { my $cb = shift; while ( my ( $k, $v ) = each %tests ) { - ok( my $res = $cb->( GET $k), "GET $k" ); - is( $res->code, $v, "code $v" ); + ok( my $res = $cb->( GET $k ), "GET $k" ); + is( $res->code, $v->{code}, "code " . $v->{code} ); + + test_cache_headers( $res, $v ); + if ( $k eq '/source/Moose' ) { like( $res->content, qr/package Moose/, 'Moose source' ); - is( $res->header('content-type'), - 'text/plain; charset=UTF-8', - 'Content-type' - ); + is( $res->header('content-type'), 'text/plain', 'Content-type' ); + + # Used for fastly on st.aticpan.org + is( $res->header('X-Content-Type'), + 'text/x-script.perl-module', 'X-Content-Type' ); + } elsif ( $k =~ /MANIFEST/ ) { - my $manifest = "MANIFEST\nlib/Moose.pm\nMakefile.PL\n" - . "META.yml\nt/00-nop.t"; - if( $k =~ /callback=foo/ ) { - ok( my( $function_args ) = $res->content =~ /^foo\((.*)\)/s, 'JSONP wrapper'); - ok( my $jsdata = JSON->new->allow_nonref->decode( $function_args ), 'decode json' ); + + # No EOL. + my $manifest = join( + "\n", qw( + MANIFEST + lib/Moose.pm + Makefile.PL + t/00-nop.t + META.json + META.yml + ) + ); + if ( $k =~ /callback=foo/ ) { + ok( + my ($function_args) + = $res->content =~ /^\/\*\*\/foo\((.*)\)/s, + 'JSONP wrapper' + ); + ok( + my $jsdata = Cpanel::JSON::XS->new->allow_nonref->decode( + $function_args), + 'decode json' + ); is( $jsdata, $manifest, 'JSONP-wrapped manifest' ); - is( $res->header('content-type'), + is( + $res->header('content-type'), 'text/javascript; charset=UTF-8', 'Content-type' ); @@ -41,37 +115,50 @@ test_psgi app, sub { else { is( $res->content, $manifest, 'Plain text manifest' ); is( $res->header('content-type'), - 'text/plain; charset=UTF-8', - 'Content-type' - ); + 'text/plain', 'Content-type' ); } } elsif ( $k eq '/source/DOY/Moose-0.01/Changes' ) { - is( $res->header('content-type'), - 'text/plain; charset=UTF-8', - 'Content-type' + is( $res->header('content-type'), 'text/plain', 'Content-type' ); + like( + $res->decoded_content, + qr/codename 'M\x{fc}nchen'/, + 'Change-log content' ); - like( $res->decoded_content, qr/codename 'M\x{fc}nchen'/, 'Change-log content' ); } elsif ( $k eq '/source/DOY/Moose-0.01/Changes?callback=foo' ) { - is( $res->header('content-type'), + is( + $res->header('content-type'), 'text/javascript; charset=UTF-8', 'Content-type' ); - ok( my( $function_args ) = $res->content =~ /^foo\((.*)\)/s, 'JSONP wrapper'); - ok( my $jsdata = JSON->new->allow_nonref->decode( $function_args ), 'decode json' ); - like( $jsdata, qr/codename 'M\x{fc}nchen'/, 'JSONP-wrapped change-log' ); + ok( + my ($function_args) + = $res->content =~ /^\/\*\*\/foo\((.*)\)/s, + 'JSONP wrapper' + ); + ok( + my $jsdata = JSON->new->allow_nonref->decode($function_args), + 'decode json' + ); + like( + $jsdata, + qr/codename 'M\x{fc}nchen'/, + 'JSONP-wrapped change-log' + ); } - elsif ( $v eq 200 ) { + elsif ( $v->{code} eq 200 ) { like( $res->content, qr/Index of/, 'Index of' ); - is( $res->header('content-type'), + is( + $res->header('content-type'), 'text/html; charset=UTF-8', 'Content-type' ); } else { - is( $res->header('content-type'), + is( + $res->header('content-type'), 'application/json; charset=utf-8', 'Content-type' ); diff --git a/t/server/controller/url_parameters.pm b/t/server/controller/url_parameters.pm new file mode 100644 index 000000000..58639a356 --- /dev/null +++ b/t/server/controller/url_parameters.pm @@ -0,0 +1,63 @@ +use strict; +use warnings; + +use Cpanel::JSON::XS (); +use HTTP::Request::Common qw( GET ); +use MetaCPAN::Server (); +use MetaCPAN::TestHelpers qw( test_cache_headers ); +use Plack::Test (); +use Ref::Util qw( is_arrayref is_hashref ); +use Test::More; + +my $app = MetaCPAN::Server->new->to_app(); +my $test = Plack::Test->create($app); + +subtest "parem 'source'" => sub { + my $source = Cpanel::JSON::XS::encode_json { + query => { + term => { distribution => "Moose" } + }, + aggs => { + count => { + terms => { field => "distribution" } + } + }, + size => 0, + }; + + # test different types, as the parameter is generic to all + for ( [ release => 2 ], [ file => 27 ] ) { + my ( $type, $count ) = @$_; + + my $url = "/$type/_search?source=$source"; + + subtest "check with '$type' controller" => sub { + my $res = $test->request( GET $url ); + ok( $res, "GET $url" ); + is( $res->code, 200, "code 200" ); + test_cache_headers( + $res, + { + cache_control => 'private', + surrogate_key => + 'content_type=application/json content_type=application', + surrogate_control => undef, + + } + ); + is( + $res->header('content-type'), + 'application/json; charset=utf-8', + 'Content-type' + ); + my $content = Cpanel::JSON::XS::decode_json $res->content; + ok( is_hashref($content), 'content is a JSON object' ); + my $buckets = $content->{aggregations}{count}{buckets}; + ok( is_arrayref($buckets), 'we have aggregation buckets' ); + is( @{$buckets}, 1, 'one key (Moose)' ); + is( $buckets->[0]{doc_count}, $count, "record count is $count" ); + }; + } +}; + +done_testing; diff --git a/t/server/controller/user/favorite.t b/t/server/controller/user/favorite.t index e7222fb20..6aebeccad 100644 --- a/t/server/controller/user/favorite.t +++ b/t/server/controller/user/favorite.t @@ -1,35 +1,57 @@ - use strict; use warnings; +use lib 't/lib'; + +use MetaCPAN::Server::Test qw( app DELETE GET POST test_psgi ); +use MetaCPAN::TestHelpers qw( decode_json_ok encode_json ); use Test::More; -use MetaCPAN::Server::Test; test_psgi app, sub { my $cb = shift; ok( my $user = $cb->( GET '/user?access_token=testing' ), 'get user' ); is( $user->code, 200, 'code 200' ); - ok( $user = decode_json( $user->content ), 'decode json' ); + $user = decode_json_ok($user); + + is_deeply( + $user->{identity}, + [ { + 'key' => 'MO', + 'name' => 'pause' + } ], + 'got correct identity' + ); + + is_deeply( + $user->{access_token}, + [ { + 'client' => 'testing', + 'token' => 'testing' + } ], + 'got correct access_token' + ); - ok( my $res = $cb->( + ok( + my $res = $cb->( POST '/user/favorite?access_token=testing', - Content => encode_json( - { distribution => 'Moose', - release => 'Moose-1.10', - author => 'DOY' - } - ) + Content_Type => 'application/json', + Content => encode_json( { + distribution => 'Moose', + release => 'Moose-1.10', + author => 'DOY' + } ) ), - "POST favorite" + 'POST favorite' ); is( $res->code, 201, 'status created' ); - ok( my $location = $res->header('location'), "location header set" ); + ok( my $location = $res->header('location'), 'location header set' ); ok( $res = $cb->( GET $location ), "GET $location" ); is( $res->code, 200, 'found' ); - my $json = decode_json( $res->content ); + + my $json = decode_json_ok($res); is( $json->{user}, $user->{id}, 'user is ' . $user->{id} ); - ok( $res = $cb->( DELETE "/user/favorite/Moose?access_token=testing" ), - "DELETE /user/favorite/MO/Moose" ); + ok( $res = $cb->( DELETE '/user/favorite/Moose?access_token=testing' ), + 'DELETE /user/favorite/MO/Moose' ); is( $res->code, 200, 'status ok' ); ok( $res = $cb->( GET "$location?access_token=testing" ), "GET $location" ); @@ -37,22 +59,6 @@ test_psgi app, sub { ok( $user = $cb->( GET '/user?access_token=bot' ), 'get bot' ); is( $user->code, 200, 'code 200' ); - ok( $user = decode_json( $user->content ), 'decode json' ); - ok( !$user->{looks_human}, 'user looks like a bot' ); - ok( $res = $cb->( - POST '/user/favorite?access_token=bot', - Content => encode_json( - { distribution => 'Moose', - release => 'Moose-1.10', - author => 'DOY' - } - ) - ), - "POST favorite" - ); - ok( decode_json( $res->content ), 'decode response' ); - is( $res->code, 403, 'forbidden' ); - }; done_testing; diff --git a/t/server/controller/user/turing.t b/t/server/controller/user/turing.t deleted file mode 100644 index 6b62c3260..000000000 --- a/t/server/controller/user/turing.t +++ /dev/null @@ -1,47 +0,0 @@ -package Captcha::Mock; - -sub check_answer { - return { is_valid => $_[4], error => 'error' }; -} - -sub new { - bless {}, shift; -} - -package main; -use strict; -use warnings; -use Test::More; -use MetaCPAN::Server::Test; - -test_psgi app, sub { - my $cb = shift; - ok( my $res = $cb->( - POST '/user/turing?access_token=testing', - Content => encode_json( - { challenge => "foo", - answer => 0 - } - ) - ), - 'post challenge' - ); - is( $res->code, 400, "bad request" ); - - ok( $res = $cb->( - POST '/user/turing?access_token=testing', - Content => encode_json( - { challenge => "foo", - answer => 1, - } - ) - ), - 'post challenge' - ); - is( $res->code, 200, "bad request" ); - my $user = decode_json( $res->content ); - ok( $user->{looks_human}, 'looks human' ); - ok( $user->{passed_captcha}, 'passed captcha' ); -}; - -done_testing; diff --git a/t/server/model/file.t b/t/server/model/file.t deleted file mode 100644 index f88fbc228..000000000 --- a/t/server/model/file.t +++ /dev/null @@ -1,47 +0,0 @@ -use strict; -use warnings; -use Test::More; -use MetaCPAN::Server (); -my $c = 'MetaCPAN::Server'; - -foreach my $test ( - [ - LOCAL => 'Multiple-Modules-0.1', - [qw( Multiple::Modules Multiple::Modules::Deprecated )], - [] - ], - [ - LOCAL => 'Multiple-Modules-1.01', - [qw( Multiple::Modules Multiple::Modules::A Multiple::Modules::A2 Multiple::Modules::B )], - [qw( Multiple::Modules::B::Secret )] - ], - [ - LOCAL => 'Multiple-Modules-RDeps-2.03', - [qw( Multiple::Modules::RDeps )], - [] - ], - [ - LOCAL => 'Multiple-Modules-RDeps-A-2.03', - [qw( Multiple::Modules::RDeps::A )], - [] - ], -){ - my ( $author, $release, $indexed, $extra ) = @$test; - my $find = { author => $author, name => $release }; - is_deeply - [ - sort - map { $_->{name} } - map { @{ $_->{_source}->{module} } } - @{ $c->model('CPAN::File')->raw->find_provided_by( $find )->{hits}{hits} } - ], - [ sort( @$indexed, @$extra ) ], - 'got all included modules'; - - is_deeply - [ sort $c->model('CPAN::File')->raw->find_module_names_provided_by( $find ) ], - [ sort @$indexed ], - 'got only the module names expected'; -} - -done_testing; diff --git a/t/server/not_found.t b/t/server/not_found.t new file mode 100644 index 000000000..ee2666d28 --- /dev/null +++ b/t/server/not_found.t @@ -0,0 +1,43 @@ +use strict; +use warnings; +use lib 't/lib'; + +use MetaCPAN::Server::Test qw( app GET test_psgi ); +use MetaCPAN::TestHelpers qw( decode_json_ok ); +use Test::More; + +my @tests = ( + [ '/changes/LOCAL/File-Changes-2' => 404 ], + [ '/changes/LOCAL/File-Changes-2.0' => 200 ], + [ '/fakedoctype/andaction' => 404 ], + [ '/file/LOCAL/File-Changes-2.0/Changes' => 200 ], + [ '/file/LOCAL/File-Changes-2.0/NoChanges' => 404 ], + [ '/release/File-Changes' => 200 ], + [ '/release/No-Dist-Here' => 404 ], + [ '/root.file' => 404 ], +); + +test_psgi app, sub { + my $cb = shift; + for my $test (@tests) { + my ( $path, $code ) = @{$test}; + + ok( my $res = $cb->( GET $path ), "GET $path" ); + is( $res->code, $code, "code $code" ); + + # 404 should still be json + is( + $res->header('content-type'), + 'application/json; charset=utf-8', + 'Content-type' + ); + my $json = decode_json_ok($res); + + next unless $res->code == 404; + + is( $json->{message}, 'Not found', '404 message as expected' ); + is( $json->{code}, $code, 'code as expected' ); + } +}; + +done_testing; diff --git a/t/server/sanitize_query.t b/t/server/sanitize_query.t new file mode 100644 index 000000000..d843eeb33 --- /dev/null +++ b/t/server/sanitize_query.t @@ -0,0 +1,147 @@ +use strict; +use warnings; +use lib 't/lib'; + +use MetaCPAN::Server::Test qw( app GET POST test_psgi ); +use MetaCPAN::TestHelpers qw( catch decode_json_ok encode_json try ); +use Test::More skip_all => 'Scripting is disabled'; +use URI (); + +sub uri { + my $uri = URI->new(shift); + $uri->query_form( {@_} ); + return $uri->as_string; +} + +sub like_if_defined { + my ( $val, $exp, $desc ) = @_; + defined($exp) ? like( $val, $exp, $desc ) : is( $val, undef, $desc ); +} + +my $error_message = 'Parameter "script" not allowed'; + +my %errors = ( + 'filter:script' => + '{"query":{"match_all":{}},"filter":{"script":{"script":"true"}}}', + 'filtered query custom_score' => { + filtered => { + query => { + custom_score => { + query => { who => 'cares' }, + script => 'anything', + }, + }, + filter => { dont => 'care' }, + } + }, +); + +test_psgi app, sub { + my $cb = shift; + while ( my ( $desc, $search ) = each %errors ) { + test_all_methods( + $search, + sub { + my ($req) = shift; + test_bad_request( $cb, $desc, $search, $req ); + } + ); + } +}; + +sub test_all_methods { + my ( $search, $sub ) = @_; + $search = encode_json($search) if ref($search) eq 'HASH'; + + foreach my $req ( + POST( '/author/_search', Content => $search ), + GET( uri( '/author/_search', source => $search ) ), + ) + { + $sub->($req); + } +} + +sub test_bad_request { + my ( $cb, $desc, $search, $req ) = @_; + my $method = $req->method; + subtest "bad request for $method '$desc'" => sub { + if ( $method eq 'GET' ) { + like $req->uri, qr/\?source=%7B%22(query|filtered)%22%3A/, + 'uri has json in querystring'; + } + else { + like $req->content, qr/{"(query|filtered)":/, 'body is json'; + } + + ok( my $res = $cb->($req), "$method request" ); + + is $res->code, 403, 'Not allowed'; + + my $json = decode_json_ok($res) + or diag explain $res; + + is_deeply $json, { message => "$error_message" }, + "error returned for $desc"; + }; +} + +hash_key_rejected( script => { script => 'foobar' } ); +hash_key_rejected( + script => { tree => { of => 'many', hashes => { script => 'foobar' } } } +); +hash_key_rejected( + script => { + with => { arrays => [ { of => 'hashes' }, { script => 'foobar' } ] } + } +); + +{ + my $hash = filtered_custom_score_hash( hi => 'there' ); + + is_deeply delete $hash->{query}{filtered}{query}, + { custom_score => { query => { foo => 'bar' }, hi => 'there' } }, + 'remove custom_score hash'; + + $hash->{query}{filtered}{fooey} = {}; + + is_deeply + +MetaCPAN::Server::QuerySanitizer->new( query => $hash )->query, + { query => { filtered => { fooey => {} } }, }, + 'test that sanitizing does not autovivify hash keys'; +} + +done_testing; + +sub filtered_custom_score_hash { + return { + query => { + filtered => { + query => { + custom_score => { + query => { foo => 'bar' }, + @_ + } + } + } + } + }; +} + +sub hash_key_rejected { + my ( $key, $hash ) = @_; + my $e; + try { + MetaCPAN::Server::QuerySanitizer->new( query => $hash )->query; + } + catch { + $e = $_[0]; + }; + + if ( defined $e ) { + like $e, qr/Parameter "$key" not allowed/, "died for bad key '$key'"; + } + else { + ok 0, 'error expected but not found'; + } +} diff --git a/t/test-vars.t b/t/test-vars.t new file mode 100644 index 000000000..1787f26c4 --- /dev/null +++ b/t/test-vars.t @@ -0,0 +1,10 @@ +use strict; +use warnings; +use lib 't/lib'; + +use Test::More; +use Test::Vars import => [qw( vars_ok )]; + +vars_ok('MetaCPAN::Server'); + +done_testing(); diff --git a/t/testrules.yml b/t/testrules.yml new file mode 100644 index 000000000..f5e59287f --- /dev/null +++ b/t/testrules.yml @@ -0,0 +1,9 @@ +--- +seq: + - seq: t/0*.t + + # ensure t/script/cover.t runs before t/server/controller/cover.t + - seq: t/script/cover.t + + - par: + - t/**.t diff --git a/t/types.t b/t/types.t index 47e0492f3..3f8ce75a5 100644 --- a/t/types.t +++ b/t/types.t @@ -1,88 +1,92 @@ -use Test::Most; use strict; use warnings; -use MetaCPAN::Types qw(:all); + +use MetaCPAN::Types::TypeTiny qw( Resources ); +use Test::More; is_deeply( - Resources->coerce( - { license => ['http://dev.perl.org/licenses/'], - homepage => 'http://sourceforge.net/projects/module-build', - bugtracker => { - web => 'http://github.com/dagolden/cpan-meta-spec/issues', - mailto => 'meta-bugs@example.com', - }, - repository => { - url => 'git://github.com/dagolden/cpan-meta-spec.git', - web => 'http://github.com/dagolden/cpan-meta-spec', - type => 'git', - }, - x_twitter => 'http://twitter.com/cpan_linked/', - } - ), - { license => ['http://dev.perl.org/licenses/'], - homepage => 'http://sourceforge.net/projects/module-build', - bugtracker => { - web => 'http://github.com/dagolden/cpan-meta-spec/issues', - mailto => 'meta-bugs@example.com', - }, - repository => { - url => 'git://github.com/dagolden/cpan-meta-spec.git', - web => 'http://github.com/dagolden/cpan-meta-spec', - type => 'git', - } - }, - 'coerce CPAN::Meta::Spec example' ); + Resources->coerce( { + license => ['http://dev.perl.org/licenses/'], + homepage => 'http://sourceforge.net/projects/module-build', + bugtracker => { + web => 'http://github.com/dagolden/cpan-meta-spec/issues', + mailto => 'meta-bugs@example.com', + }, + repository => { + url => 'git://github.com/dagolden/cpan-meta-spec.git', + web => 'http://github.com/dagolden/cpan-meta-spec', + type => 'git', + }, + x_twitter => 'http://twitter.com/cpan_linked/', + } ), + { + license => ['http://dev.perl.org/licenses/'], + homepage => 'http://sourceforge.net/projects/module-build', + bugtracker => { + web => 'http://github.com/dagolden/cpan-meta-spec/issues', + mailto => 'meta-bugs@example.com', + }, + repository => { + url => 'git://github.com/dagolden/cpan-meta-spec.git', + web => 'http://github.com/dagolden/cpan-meta-spec', + type => 'git', + } + }, + 'coerce CPAN::Meta::Spec example' +); ok( - Resources->check( - Resources->coerce( - { license => ['http://dev.perl.org/licenses/'], - homepage => 'http://sourceforge.net/projects/module-build', - bugtracker => { - web => 'http://github.com/dagolden/cpan-meta-spec/issues', - mailto => 'meta-bugs@example.com', - }, - repository => { - url => 'git://github.com/dagolden/cpan-meta-spec.git', - web => 'http://github.com/dagolden/cpan-meta-spec', - type => 'git', - }, - x_twitter => 'http://twitter.com/cpan_linked/', - } ) - ), - 'check CPAN::Meta::Spec example' ); + Resources->check( Resources->coerce( { + license => ['http://dev.perl.org/licenses/'], + homepage => 'http://sourceforge.net/projects/module-build', + bugtracker => { + web => 'http://github.com/dagolden/cpan-meta-spec/issues', + mailto => 'meta-bugs@example.com', + }, + repository => { + url => 'git://github.com/dagolden/cpan-meta-spec.git', + web => 'http://github.com/dagolden/cpan-meta-spec', + type => 'git', + }, + x_twitter => 'http://twitter.com/cpan_linked/', + } ) ), + 'check CPAN::Meta::Spec example' +); is_deeply( - Resources->coerce( - { license => ['http://dev.perl.org/licenses/'], - homepage => 'http://sourceforge.net/projects/module-build', - } - ), - { homepage => 'http://sourceforge.net/projects/module-build', - license => ['http://dev.perl.org/licenses/'], - }, - 'coerce sparse resources' ); + Resources->coerce( { + license => ['http://dev.perl.org/licenses/'], + homepage => 'http://sourceforge.net/projects/module-build', + } ), + { + homepage => 'http://sourceforge.net/projects/module-build', + license => ['http://dev.perl.org/licenses/'], + }, + 'coerce sparse resources' +); ok( - Resources->check( - { license => ['http://dev.perl.org/licenses/'], - homepage => 'http://sourceforge.net/projects/module-build', - } - ), - 'check sparse resources' ); + Resources->check( { + license => ['http://dev.perl.org/licenses/'], + homepage => 'http://sourceforge.net/projects/module-build', + } ), + 'check sparse resources' +); ok( - Resources->check( - { - bugtracker => { - web => - "https://github.com/AlexBio/Dist-Zilla-Plugin-GitHub/issues" - }, - homepage => "http://search.cpan.org/dist/Dist-Zilla-Plugin-GitHub/", - repository => { - type => "git", - url => "git://github.com/AlexBio/Dist-Zilla-Plugin-GitHub.git", - web => "https://github.com/AlexBio/Dist-Zilla-Plugin-GitHub" - } } ), 'sparse'); + Resources->check( { + bugtracker => { + web => + 'https://github.com/AlexBio/Dist-Zilla-Plugin-GitHub/issues' + }, + homepage => 'http://search.cpan.org/dist/Dist-Zilla-Plugin-GitHub/', + repository => { + type => 'git', + url => 'git://github.com/AlexBio/Dist-Zilla-Plugin-GitHub.git', + web => 'https://github.com/AlexBio/Dist-Zilla-Plugin-GitHub' + } + } ), + 'sparse' +); - done_testing; +done_testing; diff --git a/t/util.t b/t/util.t index 93f120b45..e53d06014 100644 --- a/t/util.t +++ b/t/util.t @@ -1,38 +1,89 @@ -use Test::Most; use strict; use warnings; -use MetaCPAN::Util; -use CPAN::Meta; - -is( MetaCPAN::Util::numify_version(1), 1.000 ); -is( MetaCPAN::Util::numify_version('010'), 10.000 ); -is( MetaCPAN::Util::numify_version('v2.1.1'), 2.001001 ); -is( MetaCPAN::Util::numify_version(undef), 0.000 ); -is( MetaCPAN::Util::numify_version('LATEST'), 0.000 ); -is( MetaCPAN::Util::numify_version('0.20_8'), 0.20008 ); -is( MetaCPAN::Util::numify_version('0.20_108'), 0.2000108 ); - -lives_ok { is(version("2a"), 2) }; -lives_ok { is(version("V0.01"), 0.01) }; -lives_ok { is(version('0.99_1'), '0.99001') }; -lives_ok { is(version('0.99.01'), '0.99.01') }; - -is(MetaCPAN::Util::strip_pod('hello L foo'), 'hello link foo'); -is(MetaCPAN::Util::strip_pod('hello L foo'), 'hello section in Module foo'); -is(MetaCPAN::Util::strip_pod('for L'), 'for Dist::Zilla'); -is(MetaCPAN::Util::strip_pod('without a leading C<$>.'), 'without a leading $.'); +use lib 't/lib'; + +use CPAN::Meta (); +use MetaCPAN::Util qw( + extract_section + generate_sid + numify_version + strip_pod +); + +use Test::Fatal qw( exception ); +use Test::More; + +ok( generate_sid(), 'generate_sid' ); + +{ + my %versions = ( + '010' => 10, + '0.20_8' => 0.208, + '0.208_8' => 0.2088, + '0.20_88' => 0.2088, + 1 => 1, + LATEST => 0, + undef => 0, + 'v0.9_9' => 0.099, + 'v2.1.1' => 2.001001, + 'v2.0.0' => 2.0, + ); + + foreach my $before ( sort keys %versions ) { + is( numify_version($before), $versions{$before}, + "$before => $versions{$before}" ); + } +} + +{ + my %versions = ( + '2a' => 2, + 'V0.01' => 'v0.01', + '0.99_1' => '0.99_1', + '0.99.01' => 'v0.99.01', + 'v1.2' => 'v1.2', + ); + foreach my $before ( sort keys %versions ) { + is exception { + is( version($before), $versions{$before}, + "$before => $versions{$before}" ) + }, undef, "$before => $versions{$before} does not die"; + } +} + +is( + strip_pod('hello L foo'), + 'hello link foo', + 'strip_pod strips http links' +); +is( + strip_pod('hello L foo'), + 'hello section in Module foo', + 'strip_pod strips internal links' +); +is( + strip_pod('for L'), + 'for Dist::Zilla', + 'strip_pod strips module links' +); +is( + strip_pod('without a leading C<$>.'), + 'without a leading $.', + 'strip_pod strips C<>' +); sub version { - CPAN::Meta->new( - { name => 'foo', - license => 'unknown', - version => MetaCPAN::Util::fix_version(shift) } )->version; + CPAN::Meta->new( { + name => 'foo', + license => 'unknown', + version => MetaCPAN::Util::fix_version(shift) + } )->version; } # extract_section tests { - my $content = <doy@cpan.org 1 + + RWSTAUNER + author + Trouble Maker + rwstauner@cpan.org + 1 + + + BORISNAT + author + Лось и Белка + Moose and Squirrel + borisnat@cpan.org + 1 + diff --git a/test-data/fakecpan/08pumpkings.txt.gz b/test-data/fakecpan/08pumpkings.txt.gz new file mode 100644 index 000000000..8493534c1 Binary files /dev/null and b/test-data/fakecpan/08pumpkings.txt.gz differ diff --git a/t/var/fakecpan/author-1.0.json b/test-data/fakecpan/author-1.0.json similarity index 100% rename from t/var/fakecpan/author-1.0.json rename to test-data/fakecpan/author-1.0.json diff --git a/test-data/fakecpan/bugs.tsv b/test-data/fakecpan/bugs.tsv new file mode 100644 index 000000000..4aa0e76e3 --- /dev/null +++ b/test-data/fakecpan/bugs.tsv @@ -0,0 +1,6 @@ +# A fake https://rt.cpan.org/Public/bugs-per-dist.tsv +# dist new open stalled patched resolved rejected active inactive +Monkey-Patch 0 0 0 0 1 0 0 1 +Moo 2 5 0 0 2 1 7 3 +Moose 15 20 4 0 122 23 39 145 +Text-Tabs+Wrap 2 0 0 0 15 1 2 16 diff --git a/test-data/fakecpan/configs/MIYAGAWA_CPAN-Test-Dummy-Perl5-VersionBump-0.01.tar.gz.dist b/test-data/fakecpan/configs/MIYAGAWA_CPAN-Test-Dummy-Perl5-VersionBump-0.01.tar.gz.dist new file mode 100644 index 000000000..b70271c26 Binary files /dev/null and b/test-data/fakecpan/configs/MIYAGAWA_CPAN-Test-Dummy-Perl5-VersionBump-0.01.tar.gz.dist differ diff --git a/test-data/fakecpan/configs/MIYAGAWA_CPAN-Test-Dummy-Perl5-VersionBump-0.02.tar.gz.dist b/test-data/fakecpan/configs/MIYAGAWA_CPAN-Test-Dummy-Perl5-VersionBump-0.02.tar.gz.dist new file mode 100644 index 000000000..b68e49094 Binary files /dev/null and b/test-data/fakecpan/configs/MIYAGAWA_CPAN-Test-Dummy-Perl5-VersionBump-0.02.tar.gz.dist differ diff --git a/test-data/fakecpan/configs/badpod.json b/test-data/fakecpan/configs/badpod.json new file mode 100644 index 000000000..76f7c4d1b --- /dev/null +++ b/test-data/fakecpan/configs/badpod.json @@ -0,0 +1,12 @@ +{ + "name": "BadPod", + "abstract": "Distribution with malformed POD", + "X_Module_Faker": { + "cpan_author": "MO", + "omitted_files": ["META.json", "META.yml"], + "append": [ { + "file": "lib/BadPod.pm", + "content": "\n\n=head1 NAME\n\nBadPod - Malformed POD\n\n=head SYNOPSIS\n\nThere is no C 'Binary-Data', + abstract => 'Binary after __DATA__ token', + version => '0.01', + + # Specify provides so that both modules are included + # in release 'provides' list and the release will get marked as latest. + provides => { + 'Binary::Data' => { + file => 'lib/Binary/Data.pm', + version => '0.01' + }, + 'Binary::Data::WithPod' => { + file => 'lib/Binary/Data/WithPod.pm', + version => '0.02' + } + }, + + X_Module_Faker => { + cpan_author => 'BORISNAT', + append => [ + { + file => 'lib/Binary/Data.pm', + content => < 'lib/Binary/Data/WithPod.pm', + 'content' => <