Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*]
indent_style = space
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf
20 changes: 20 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[flake8]
max_line_length = 150
# Google docstring convention + D204 & D401
# TODO enable D100 & D401
docstring-convention=all
ignore=
D100
D104
D203
D213
D215
D400
D404
D406
D407
D408
D409
D413
per-file-ignores =
tests/*: D
32 changes: 32 additions & 0 deletions .github/workflows/lint_and_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Lint and test

on: push

jobs:
build:
runs-on: ubuntu-20.04
strategy:
matrix:
python-version: [3.7, 3.8, 3.9]

steps:
- uses: actions/checkout@v2

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[dev]

- name: Lint
run: ./lint_and_test.sh lint

- name: Type check
run: ./lint_and_test.sh types

- name: Unit tests
run: ./lint_and_test.sh tests
14 changes: 11 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
build
dist
apify_client.egg-info
.venv
.direnv
.envrc

.mypy_cache
.pytest_cache

*.egg-info/
*.egg

.vscode
.idea
6 changes: 6 additions & 0 deletions .isort.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[isort]
include_trailing_comma = True
line_length = 150
use_parentheses = True
multi_line_output = 3
sections = FUTURE,STDLIB,FIRSTPARTY,THIRDPARTY,LOCALFOLDER
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright 2018 Apify Technologies s.r.o.
Copyright 2020 Apify Technologies s.r.o.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,41 @@
This will be an official client for [Apify API](https://www.apify.com/docs/api/v2).
It's still work in progress, so please don't use it yet!

## Installation

Requires Python 3.7+
Comment thread
fnesveda marked this conversation as resolved.
Resolved

Right now the client is not available on PyPI yet, so you can install it only from the git repo.
To do that, run `pip install git+https://github.com/apify/apify-client-python.git`

## Development

### Environment

For local development, it is required to have Python 3.7 installed.

It is recommended to set up a virtual environment while developing this package to isolate your development environment,
however, due to the many varied ways Python can be installed and virtual environments can be set up,
this is left up to the developers to do themselves.

One recommended way is with the builtin `venv` module:

```bash
python3 -m venv .venv
source .venv/bin/activate
```

To improve on the experience, you can use [pyenv](https://github.com/pyenv/pyenv) to have an environment with a pinned Python version,
and [direnv](https://github.com/direnv/direnv) to automatically activate/deactivate the environment when you enter/exit the project folder.

### Dependencies

To install this package and its development dependencies, run `pip install -e '.[dev]'`

### Formatting

We use `autopep8` and `isort` to automatically format the code to a common format. To run the formatting, just run `./format.sh`.

### Linting and Testing

We use `flake8` for linting, `mypy` for type checking and `pytest` for unit testing. To run these tools, just run `./lint_and_test.sh`.
1 change: 0 additions & 1 deletion apify_client/__init__.py

This file was deleted.

11 changes: 11 additions & 0 deletions format.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env /bin/bash

set -o errexit

echo "🔤 isort formatting..."
python3 -m isort src tests

echo "🎨 autopep8 formatting..."
python3 -m autopep8 --in-place --recursive src tests

echo "🎉 formatted 🎉"
40 changes: 40 additions & 0 deletions lint_and_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash

set -u

checks_to_run=${1:-"everything"}
shift || true


lint() {
echo "⛄️ flake8 check..."
python3 -m flake8 src tests
Comment thread
fnesveda marked this conversation as resolved.
Resolved
}

type_check() {
echo "📝 checking types"
python3 -m mypy src
}

unit_tests() {
echo "👮‍♀️ running unit tests"
python3 -m pytest -rA tests
}


if [ "$checks_to_run" = "lint" ] ; then
lint
elif [ "$checks_to_run" = "types" ] ; then
type_check
elif [ "$checks_to_run" = "tests" ] ; then
unit_tests
elif [ "$checks_to_run" = "everything" ] ; then
lint
type_check
unit_tests
else
echo "Invalid type of test ($checks_to_run) requested. Use lint / types / tests or leave empty to run all tests"
exit 1
fi

echo "🎉 success 🎉"
11 changes: 11 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[mypy]
check_untyped_defs = True
disallow_incomplete_defs = True
disallow_untyped_calls = True
disallow_untyped_decorators = True
disallow_untyped_defs = True
no_implicit_optional = True
warn_redundant_casts = True
warn_return_any = True
warn_unreachable = True
warn_unused_ignores = True
2 changes: 2 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
addopts = --doctest-modules
54 changes: 42 additions & 12 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,52 @@
import setuptools
import pathlib

with open("README.md", "r") as fh:
long_description = fh.read()
from setuptools import find_packages, setup

here = pathlib.Path(__file__).parent.resolve()

long_description = (here / 'README.md').read_text(encoding='utf-8')

setup(
name='apify_client',
version='0.0.1',

setuptools.setup(
name="apify_client",
version="0.0.1",
author="Apify Technologies s.r.o.",
author_email="support@apify.com",
description="Work in progress: Apify API client for Python",
url="https://github.com/apify/apify-client-python",
project_urls={
'Apify Homepage': 'https://apify.com',
},
license='Apache Software License',
license_file='LICENSE',

description='Apify API client for Python',
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/apifytech/apify-client-python",
packages=setuptools.find_packages(),
long_description_content_type='text/markdown',

classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
'Intended Audience :: Developers',
'Topic :: Software Development :: Libraries',
],
)
keywords='apify, api, client, scraping, automation',

package_dir={'': 'src'},
packages=find_packages(where='src'),
python_requires='>=3.7',
install_requires=['requests ~=2.25.1'],
extras_require={
'dev': [
'autopep8 ~= 1.5.4',
'flake8 ~= 3.8.4',
'flake8-commas ~= 2.0.0',
'flake8-docstrings ~= 1.5.0',
'flake8-isort ~= 4.0.0',
'isort ~= 5.7.0',
'mypy ~= 0.790',
'pep8-naming ~= 0.11.1',
'pytest ~= 6.2.1',
],
},
)
3 changes: 3 additions & 0 deletions src/apify_client/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .client import ApifyClient

__all__ = ['ApifyClient']
69 changes: 69 additions & 0 deletions src/apify_client/_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from typing import Optional

import requests


class ApifyClientError(Exception):
"""Base class for errors specific to the Apify API Client."""

pass


class ApifyApiError(ApifyClientError):
"""Error specific to requests to the Apify API.

An `ApifyApiError` is thrown for successful HTTP requests that reach the API,
but the API responds with an error response. Typically, those are rate limit
errors and internal errors, which are automatically retried, or validation
errors, which are thrown immediately, because a correction by the user is needed.
"""

def __init__(self, response: requests.models.Response, attempt: int) -> None:
"""Create the ApifyApiError instance.

Args:
response: The response to the failed API call
attempt: Which retry was the request that failed
"""
self.message: Optional[str] = None
self.type: Optional[str] = None

response_data = response.json()
if 'error' in response_data:
self.message = response_data['error']['message']
self.type = response_data['error']['type']
else:
self.message = f'Unexpected error: {response.text}'

super().__init__(self.message)

self.name = 'ApifyApiError'
self.status_code = response.status_code
self.attempt = attempt
self.http_method = response.request.method

# TODO self.client_method
# TODO self.original_stack
# TODO self.path
# TODO self.stack


class InvalidResponseBodyError(ApifyClientError):
"""Error caused by the response body failing to be parsed.

This error exists for the quite common situation, where only a partial JSON response is received and
an attempt to parse the JSON throws an error. In most cases this can be resolved by retrying the
request. We do that by identifying this error in the _HTTPClient.
"""

def __init__(self, response: requests.models.Response) -> None:
"""Create the InvalidResponseBodyError instance.

Args:
response: The response which failed to be parsed
"""
super().__init__('Response body could not be parsed')

self.name = 'InvalidResponseBodyError'
self.code = 'invalid-response-body'
self.response = response
Loading