Merge pull request #53 from anchore/add-python-wheel-egg-analyzers

Add python wheel and egg analyzer support
This commit is contained in:
Alex Goodman 2020-06-05 09:59:27 -04:00 committed by GitHub
commit 64a9125895
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 622 additions and 15 deletions

View file

@ -13,7 +13,7 @@ type Analyzer struct {
func NewAnalyzer() *Analyzer { func NewAnalyzer() *Analyzer {
globParserDispatch := map[string]common.ParserFn{ globParserDispatch := map[string]common.ParserFn{
"*/Gemfile.lock": ParseGemfileLockEntries, "*/Gemfile.lock": parseGemfileLockEntries,
} }
return &Analyzer{ return &Analyzer{

View file

@ -11,7 +11,7 @@ import (
var sectionsOfInterest = internal.NewStringSetFromSlice([]string{"GEM"}) var sectionsOfInterest = internal.NewStringSetFromSlice([]string{"GEM"})
func ParseGemfileLockEntries(reader io.Reader) ([]pkg.Package, error) { func parseGemfileLockEntries(reader io.Reader) ([]pkg.Package, error) {
pkgs := make([]pkg.Package, 0) pkgs := make([]pkg.Package, 0)
scanner := bufio.NewScanner(reader) scanner := bufio.NewScanner(reader)

View file

@ -67,7 +67,7 @@ func TestParseGemfileLockEntries(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err) t.Fatalf("failed to open fixture: %+v", err)
} }
actual, err := ParseGemfileLockEntries(fixture) actual, err := parseGemfileLockEntries(fixture)
if err != nil { if err != nil {
t.Fatalf("failed to parse gemfile lock: %+v", err) t.Fatalf("failed to parse gemfile lock: %+v", err)
} }

View file

@ -9,6 +9,8 @@ import (
"github.com/anchore/stereoscope/pkg/tree" "github.com/anchore/stereoscope/pkg/tree"
) )
// TODO: put under test...
type GenericAnalyzer struct { type GenericAnalyzer struct {
globParserDispatch map[string]ParserFn globParserDispatch map[string]ParserFn
pathParserDispatch map[string]ParserFn pathParserDispatch map[string]ParserFn

View file

@ -3,6 +3,7 @@ package analyzer
import ( import (
"github.com/anchore/imgbom/imgbom/analyzer/bundler" "github.com/anchore/imgbom/imgbom/analyzer/bundler"
"github.com/anchore/imgbom/imgbom/analyzer/dpkg" "github.com/anchore/imgbom/imgbom/analyzer/dpkg"
"github.com/anchore/imgbom/imgbom/analyzer/python"
"github.com/anchore/imgbom/imgbom/pkg" "github.com/anchore/imgbom/imgbom/pkg"
"github.com/anchore/imgbom/imgbom/scope" "github.com/anchore/imgbom/imgbom/scope"
"github.com/anchore/imgbom/internal/log" "github.com/anchore/imgbom/internal/log"
@ -30,6 +31,7 @@ func newController() controller {
} }
ctrlr.add(dpkg.NewAnalyzer()) ctrlr.add(dpkg.NewAnalyzer())
ctrlr.add(bundler.NewAnalyzer()) ctrlr.add(bundler.NewAnalyzer())
ctrlr.add(python.NewAnalyzer())
return ctrlr return ctrlr
} }

View file

@ -13,7 +13,7 @@ type Analyzer struct {
func NewAnalyzer() *Analyzer { func NewAnalyzer() *Analyzer {
pathParserDispatch := map[string]common.ParserFn{ pathParserDispatch := map[string]common.ParserFn{
"/var/lib/dpkg/status": ParseDpkgStatus, "/var/lib/dpkg/status": parseDpkgStatus,
} }
return &Analyzer{ return &Analyzer{

View file

@ -12,7 +12,7 @@ import (
var errEndOfPackages = fmt.Errorf("no more packages to read") var errEndOfPackages = fmt.Errorf("no more packages to read")
func ParseDpkgStatus(reader io.Reader) ([]pkg.Package, error) { func parseDpkgStatus(reader io.Reader) ([]pkg.Package, error) {
buffedReader := bufio.NewReader(reader) buffedReader := bufio.NewReader(reader)
var packages = make([]pkg.Package, 0) var packages = make([]pkg.Package, 0)

View file

@ -90,7 +90,7 @@ func TestMultiplePackages(t *testing.T) {
} }
}() }()
pkgs, err := ParseDpkgStatus(file) pkgs, err := parseDpkgStatus(file)
if err != nil { if err != nil {
t.Fatal("Unable to read file contents: ", err) t.Fatal("Unable to read file contents: ", err)
} }

View file

@ -0,0 +1,35 @@
package python
import (
"github.com/anchore/imgbom/imgbom/analyzer/common"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/tree"
)
type Analyzer struct {
analyzer common.GenericAnalyzer
}
func NewAnalyzer() *Analyzer {
globParserDispatch := map[string]common.ParserFn{
"*egg-info/PKG-INFO": parseEggMetadata,
"*dist-info/METADATA": parseWheelMetadata,
}
return &Analyzer{
analyzer: common.NewGenericAnalyzer(nil, globParserDispatch),
}
}
func (a *Analyzer) Name() string {
return "python-analyzer"
}
func (a *Analyzer) SelectFiles(trees []tree.FileTreeReader) []file.Reference {
return a.analyzer.SelectFiles(trees)
}
func (a *Analyzer) Analyze(contents map[file.Reference]string) ([]pkg.Package, error) {
return a.analyzer.Analyze(contents, a.Name())
}

View file

@ -0,0 +1,90 @@
package python
import (
"bufio"
"fmt"
"io"
"strings"
"github.com/anchore/imgbom/imgbom/pkg"
)
func parseWheelMetadata(reader io.Reader) ([]pkg.Package, error) {
packages, err := parseWheelOrEggMetadata(reader)
for idx := range packages {
packages[idx].Type = pkg.WheelPkg
}
return packages, err
}
func parseEggMetadata(reader io.Reader) ([]pkg.Package, error) {
packages, err := parseWheelOrEggMetadata(reader)
for idx := range packages {
packages[idx].Type = pkg.EggPkg
}
return packages, err
}
func parseWheelOrEggMetadata(reader io.Reader) ([]pkg.Package, error) {
fields := make(map[string]string)
var key string
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := scanner.Text()
line = strings.TrimRight(line, "\n")
// empty line indicates end of entry
if len(line) == 0 {
// if the entry has not started, keep parsing lines
if len(fields) == 0 {
continue
}
break
}
switch {
case strings.HasPrefix(line, " "):
// a field-body continuation
if len(key) == 0 {
return nil, fmt.Errorf("no match for continuation: line: '%s'", line)
}
val, ok := fields[key]
if !ok {
return nil, fmt.Errorf("no previous key exists, expecting: %s", key)
}
// concatenate onto previous value
val = fmt.Sprintf("%s\n %s", val, strings.TrimSpace(line))
fields[key] = val
default:
// parse a new key (note, duplicate keys are overridden)
if i := strings.Index(line, ":"); i > 0 {
key = strings.TrimSpace(line[0:i])
val := strings.TrimSpace(line[i+1:])
fields[key] = val
} else {
return nil, fmt.Errorf("cannot parse field from line: '%s'", line)
}
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("failed to parse python wheel/egg: %w", err)
}
p := pkg.Package{
Name: fields["Name"],
Version: fields["Version"],
Language: pkg.Python,
}
if license, ok := fields["License"]; ok && license != "" {
p.Licenses = []string{license}
}
return []pkg.Package{p}, nil
}

View file

@ -0,0 +1,91 @@
package python
import (
"os"
"testing"
"github.com/anchore/imgbom/imgbom/pkg"
)
func assertPkgsEqual(t *testing.T, actual []pkg.Package, expected map[string]pkg.Package) {
t.Helper()
if len(actual) != 1 {
for _, a := range actual {
t.Log(" ", a)
}
t.Fatalf("unexpected package count: %d!=%d", len(actual), 1)
}
for _, a := range actual {
expectedPkg, ok := expected[a.Name]
if !ok {
t.Errorf("unexpected package found: '%s'", a.Name)
}
if expectedPkg.Version != a.Version {
t.Errorf("unexpected package version: '%s'", a.Version)
}
if a.Language != expectedPkg.Language {
t.Errorf("bad language: '%+v'", a.Language)
}
if a.Type != expectedPkg.Type {
t.Errorf("bad package type: %+v", a.Type)
}
if len(a.Licenses) < 1 {
t.Errorf("bad package licenses count: '%+v'", a.Licenses)
} else if a.Licenses[0] != expectedPkg.Licenses[0] {
t.Errorf("bad package licenses: '%+v'", a.Licenses)
}
}
}
func TestParseEggMetadata(t *testing.T) {
expected := map[string]pkg.Package{
"requests": {
Name: "requests",
Version: "2.22.0",
Language: pkg.Python,
Type: pkg.EggPkg,
Licenses: []string{"Apache 2.0"},
},
}
fixture, err := os.Open("test-fixtures/egg-info/PKG-INFO")
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseEggMetadata(fixture)
if err != nil {
t.Fatalf("failed to parse egg-info: %+v", err)
}
assertPkgsEqual(t, actual, expected)
}
func TestParseWheelMetadata(t *testing.T) {
expected := map[string]pkg.Package{
"Pygments": {
Name: "Pygments",
Version: "2.6.1",
Language: pkg.Python,
Type: pkg.WheelPkg,
Licenses: []string{"BSD License"},
},
}
fixture, err := os.Open("test-fixtures/dist-info/METADATA")
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseWheelMetadata(fixture)
if err != nil {
t.Fatalf("failed to parse dist-info: %+v", err)
}
assertPkgsEqual(t, actual, expected)
}

View file

@ -0,0 +1,47 @@
Metadata-Version: 2.1
Name: Pygments
Version: 2.6.1
Summary: Pygments is a syntax highlighting package written in Python.
Home-page: https://pygments.org/
Author: Georg Brandl
Author-email: georg@python.org
License: BSD License
Keywords: syntax highlighting
Platform: any
Classifier: License :: OSI Approved :: BSD License
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: End Users/Desktop
Classifier: Intended Audience :: System Administrators
Classifier: Development Status :: 6 - Mature
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Operating System :: OS Independent
Classifier: Topic :: Text Processing :: Filters
Classifier: Topic :: Utilities
Requires-Python: >=3.5
Pygments
~~~~~~~~
Pygments is a syntax highlighting package written in Python.
It is a generic syntax highlighter suitable for use in code hosting, forums,
wikis or other applications that need to prettify source code. Highlights
are:
* a wide range of over 500 languages and other text formats is supported
* special attention is paid to details, increasing quality by a fair amount
* support for new languages and formats are added easily
* a number of output formats, presently HTML, LaTeX, RTF, SVG, all image formats that PIL supports and ANSI sequences
* it is usable as a command-line tool and as a library
:copyright: Copyright 2006-2019 by the Pygments team, see AUTHORS.
:license: BSD, see LICENSE for details.

View file

@ -0,0 +1,134 @@
Metadata-Version: 2.1
Name: requests
Version: 2.22.0
Summary: Python HTTP for Humans.
Home-page: http://python-requests.org
Author: Kenneth Reitz
Author-email: me@kennethreitz.org
License: Apache 2.0
Description: Requests: HTTP for Humans™
==========================
[![image](https://img.shields.io/pypi/v/requests.svg)](https://pypi.org/project/requests/)
[![image](https://img.shields.io/pypi/l/requests.svg)](https://pypi.org/project/requests/)
[![image](https://img.shields.io/pypi/pyversions/requests.svg)](https://pypi.org/project/requests/)
[![codecov.io](https://codecov.io/github/requests/requests/coverage.svg?branch=master)](https://codecov.io/github/requests/requests)
[![image](https://img.shields.io/github/contributors/requests/requests.svg)](https://github.com/requests/requests/graphs/contributors)
[![image](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/kennethreitz)
Requests is the only *Non-GMO* HTTP library for Python, safe for human
consumption.
![image](https://farm5.staticflickr.com/4317/35198386374_1939af3de6_k_d.jpg)
Behold, the power of Requests:
``` {.sourceCode .python}
>>> import requests
>>> r = requests.get('https://api.github.com/user', auth=('user', 'pass'))
>>> r.status_code
200
>>> r.headers['content-type']
'application/json; charset=utf8'
>>> r.encoding
'utf-8'
>>> r.text
u'{"type":"User"...'
>>> r.json()
{u'disk_usage': 368627, u'private_gists': 484, ...}
```
See [the similar code, sans Requests](https://gist.github.com/973705).
[![image](https://raw.githubusercontent.com/requests/requests/master/docs/_static/requests-logo-small.png)](http://docs.python-requests.org/)
Requests allows you to send *organic, grass-fed* HTTP/1.1 requests,
without the need for manual labor. There's no need to manually add query
strings to your URLs, or to form-encode your POST data. Keep-alive and
HTTP connection pooling are 100% automatic, thanks to
[urllib3](https://github.com/shazow/urllib3).
Besides, all the cool kids are doing it. Requests is one of the most
downloaded Python packages of all time, pulling in over 11,000,000
downloads every month. You don't want to be left out!
Feature Support
---------------
Requests is ready for today's web.
- International Domains and URLs
- Keep-Alive & Connection Pooling
- Sessions with Cookie Persistence
- Browser-style SSL Verification
- Basic/Digest Authentication
- Elegant Key/Value Cookies
- Automatic Decompression
- Automatic Content Decoding
- Unicode Response Bodies
- Multipart File Uploads
- HTTP(S) Proxy Support
- Connection Timeouts
- Streaming Downloads
- `.netrc` Support
- Chunked Requests
Requests officially supports Python 2.7 & 3.43.7, and runs great on
PyPy.
Installation
------------
To install Requests, simply use [pipenv](http://pipenv.org/) (or pip, of
course):
``` {.sourceCode .bash}
$ pipenv install requests
✨🍰✨
```
Satisfaction guaranteed.
Documentation
-------------
Fantastic documentation is available at
<http://docs.python-requests.org/>, for a limited time only.
How to Contribute
-----------------
1. Become more familiar with the project by reading our [Contributor's Guide](http://docs.python-requests.org/en/latest/dev/contributing/) and our [development philosophy](http://docs.python-requests.org/en/latest/dev/philosophy/).
2. Check for open issues or open a fresh issue to start a discussion
around a feature idea or a bug. There is a [Contributor
Friendly](https://github.com/requests/requests/issues?direction=desc&labels=Contributor+Friendly&page=1&sort=updated&state=open)
tag for issues that should be ideal for people who are not very
familiar with the codebase yet.
3. Fork [the repository](https://github.com/requests/requests) on
GitHub to start making your changes to the **master** branch (or
branch off of it).
4. Write a test which shows that the bug was fixed or that the feature
works as expected.
5. Send a pull request and bug the maintainer until it gets merged and
published. :) Make sure to add yourself to
[AUTHORS](https://github.com/requests/requests/blob/master/AUTHORS.rst).
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
Description-Content-Type: text/markdown
Provides-Extra: security
Provides-Extra: socks

View file

@ -5,8 +5,10 @@ const (
ApkPkg ApkPkg
BundlerPkg BundlerPkg
DebPkg DebPkg
EggPkg
PacmanPkg PacmanPkg
RpmPkg RpmPkg
WheelPkg
) )
type Type uint type Type uint
@ -14,18 +16,22 @@ type Type uint
var typeStr = []string{ var typeStr = []string{
"UnknownPackage", "UnknownPackage",
"apk", "apk",
"bundler", "bundle",
"deb", "deb",
"egg",
"pacman", "pacman",
"rpm", "rpm",
"wheel",
} }
var AllPkgs = []Type{ var AllPkgs = []Type{
ApkPkg, ApkPkg,
BundlerPkg, BundlerPkg,
DebPkg, DebPkg,
EggPkg,
PacmanPkg, PacmanPkg,
RpmPkg, RpmPkg,
WheelPkg,
} }
func (t Type) String() string { func (t Type) String() string {

View file

@ -11,7 +11,7 @@ import (
"github.com/anchore/imgbom/imgbom/scope" "github.com/anchore/imgbom/imgbom/scope"
) )
func TestBundlerImage(t *testing.T) { func TestLanguageImage(t *testing.T) {
img, cleanup := testutils.GetFixtureImage(t, "docker-archive", "image-language-pkgs") img, cleanup := testutils.GetFixtureImage(t, "docker-archive", "image-language-pkgs")
defer cleanup() defer cleanup()
@ -26,6 +26,22 @@ func TestBundlerImage(t *testing.T) {
pkgLanguage pkg.Language pkgLanguage pkg.Language
pkgInfo map[string]string pkgInfo map[string]string
}{ }{
{
name: "find python wheel packages",
pkgType: pkg.WheelPkg,
pkgLanguage: pkg.Python,
pkgInfo: map[string]string{
"Pygments": "2.6.1",
},
},
{
name: "find python egg packages",
pkgType: pkg.EggPkg,
pkgLanguage: pkg.Python,
pkgInfo: map[string]string{
"requests": "2.22.0",
},
},
{ {
name: "find bundler packages", name: "find bundler packages",
pkgType: pkg.BundlerPkg, pkgType: pkg.BundlerPkg,
@ -88,12 +104,7 @@ func TestBundlerImage(t *testing.T) {
for _, c := range cases { for _, c := range cases {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
if catalog.PackageCount() != len(c.pkgInfo) { pkgCount := 0
for a := range catalog.Enumerate(c.pkgType) {
t.Log(" ", a)
}
t.Fatalf("unexpected package count: %d!=%d", catalog.PackageCount(), len(c.pkgInfo))
}
for a := range catalog.Enumerate(c.pkgType) { for a := range catalog.Enumerate(c.pkgType) {
@ -113,6 +124,14 @@ func TestBundlerImage(t *testing.T) {
if a.Type != c.pkgType { if a.Type != c.pkgType {
t.Errorf("bad package type (pkg=%+v): %+v", a.Name, a.Type) t.Errorf("bad package type (pkg=%+v): %+v", a.Name, a.Type)
} }
pkgCount++
}
if pkgCount != len(c.pkgInfo) {
for a := range catalog.Enumerate(c.pkgType) {
t.Log(" ", a)
}
t.Fatalf("unexpected package count: %d!=%d", pkgCount, len(c.pkgInfo))
} }
}) })

View file

@ -1,2 +1,2 @@
FROM scratch FROM scratch
COPY Gemfile.lock . COPY . .

View file

@ -0,0 +1,47 @@
Metadata-Version: 2.1
Name: Pygments
Version: 2.6.1
Summary: Pygments is a syntax highlighting package written in Python.
Home-page: https://pygments.org/
Author: Georg Brandl
Author-email: georg@python.org
License: BSD License
Keywords: syntax highlighting
Platform: any
Classifier: License :: OSI Approved :: BSD License
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: End Users/Desktop
Classifier: Intended Audience :: System Administrators
Classifier: Development Status :: 6 - Mature
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Operating System :: OS Independent
Classifier: Topic :: Text Processing :: Filters
Classifier: Topic :: Utilities
Requires-Python: >=3.5
Pygments
~~~~~~~~
Pygments is a syntax highlighting package written in Python.
It is a generic syntax highlighter suitable for use in code hosting, forums,
wikis or other applications that need to prettify source code. Highlights
are:
* a wide range of over 500 languages and other text formats is supported
* special attention is paid to details, increasing quality by a fair amount
* support for new languages and formats are added easily
* a number of output formats, presently HTML, LaTeX, RTF, SVG, all image formats that PIL supports and ANSI sequences
* it is usable as a command-line tool and as a library
:copyright: Copyright 2006-2019 by the Pygments team, see AUTHORS.
:license: BSD, see LICENSE for details.

View file

@ -0,0 +1,134 @@
Metadata-Version: 2.1
Name: requests
Version: 2.22.0
Summary: Python HTTP for Humans.
Home-page: http://python-requests.org
Author: Kenneth Reitz
Author-email: me@kennethreitz.org
License: Apache 2.0
Description: Requests: HTTP for Humans™
==========================
[![image](https://img.shields.io/pypi/v/requests.svg)](https://pypi.org/project/requests/)
[![image](https://img.shields.io/pypi/l/requests.svg)](https://pypi.org/project/requests/)
[![image](https://img.shields.io/pypi/pyversions/requests.svg)](https://pypi.org/project/requests/)
[![codecov.io](https://codecov.io/github/requests/requests/coverage.svg?branch=master)](https://codecov.io/github/requests/requests)
[![image](https://img.shields.io/github/contributors/requests/requests.svg)](https://github.com/requests/requests/graphs/contributors)
[![image](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/kennethreitz)
Requests is the only *Non-GMO* HTTP library for Python, safe for human
consumption.
![image](https://farm5.staticflickr.com/4317/35198386374_1939af3de6_k_d.jpg)
Behold, the power of Requests:
``` {.sourceCode .python}
>>> import requests
>>> r = requests.get('https://api.github.com/user', auth=('user', 'pass'))
>>> r.status_code
200
>>> r.headers['content-type']
'application/json; charset=utf8'
>>> r.encoding
'utf-8'
>>> r.text
u'{"type":"User"...'
>>> r.json()
{u'disk_usage': 368627, u'private_gists': 484, ...}
```
See [the similar code, sans Requests](https://gist.github.com/973705).
[![image](https://raw.githubusercontent.com/requests/requests/master/docs/_static/requests-logo-small.png)](http://docs.python-requests.org/)
Requests allows you to send *organic, grass-fed* HTTP/1.1 requests,
without the need for manual labor. There's no need to manually add query
strings to your URLs, or to form-encode your POST data. Keep-alive and
HTTP connection pooling are 100% automatic, thanks to
[urllib3](https://github.com/shazow/urllib3).
Besides, all the cool kids are doing it. Requests is one of the most
downloaded Python packages of all time, pulling in over 11,000,000
downloads every month. You don't want to be left out!
Feature Support
---------------
Requests is ready for today's web.
- International Domains and URLs
- Keep-Alive & Connection Pooling
- Sessions with Cookie Persistence
- Browser-style SSL Verification
- Basic/Digest Authentication
- Elegant Key/Value Cookies
- Automatic Decompression
- Automatic Content Decoding
- Unicode Response Bodies
- Multipart File Uploads
- HTTP(S) Proxy Support
- Connection Timeouts
- Streaming Downloads
- `.netrc` Support
- Chunked Requests
Requests officially supports Python 2.7 & 3.43.7, and runs great on
PyPy.
Installation
------------
To install Requests, simply use [pipenv](http://pipenv.org/) (or pip, of
course):
``` {.sourceCode .bash}
$ pipenv install requests
✨🍰✨
```
Satisfaction guaranteed.
Documentation
-------------
Fantastic documentation is available at
<http://docs.python-requests.org/>, for a limited time only.
How to Contribute
-----------------
1. Become more familiar with the project by reading our [Contributor's Guide](http://docs.python-requests.org/en/latest/dev/contributing/) and our [development philosophy](http://docs.python-requests.org/en/latest/dev/philosophy/).
2. Check for open issues or open a fresh issue to start a discussion
around a feature idea or a bug. There is a [Contributor
Friendly](https://github.com/requests/requests/issues?direction=desc&labels=Contributor+Friendly&page=1&sort=updated&state=open)
tag for issues that should be ideal for people who are not very
familiar with the codebase yet.
3. Fork [the repository](https://github.com/requests/requests) on
GitHub to start making your changes to the **master** branch (or
branch off of it).
4. Write a test which shows that the bug was fixed or that the feature
works as expected.
5. Send a pull request and bug the maintainer until it gets merged and
published. :) Make sure to add yourself to
[AUTHORS](https://github.com/requests/requests/blob/master/AUTHORS.rst).
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
Description-Content-Type: text/markdown
Provides-Extra: security
Provides-Extra: socks