summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormakefu <github@syntax-fehler.de>2020-03-11 09:22:58 +0100
committermakefu <github@syntax-fehler.de>2020-03-11 09:26:51 +0100
commit1ab0949f9d8e97aafafbd347a625fd97eeaa48a3 (patch)
treebafe9c821463eb71d7944a2d9cf85082a9c6481e
parenta19ccf35dc0b01a509c88c8b66c79aa0d2a986b4 (diff)
init project setup
-rw-r--r--.envrc154
-rw-r--r--.nixpkgs-version.json7
-rw-r--r--README.md7
-rw-r--r--ebknotify/cli.py3
-rw-r--r--ebknotify/client.py227
-rw-r--r--setup.py26
-rw-r--r--shell.nix25
7 files changed, 449 insertions, 0 deletions
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..75fb474
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,154 @@
+# Load environment variables from `nix-shell` and export it out.
+#
+# Usage: use_nix [-s <nix-expression>] [-w <path>] [-w <path>] ...
+# -s nix-expression: The nix expression to use for building the shell environment.
+# -w path: watch a file for changes. It can be specified multiple times. The
+# shell specified with -s is automatically watched.
+#
+# If no nix-expression were given with -s, it will attempt to find and load
+# the shell using the following files in order: shell.nix and default.nix.
+#
+# Example:
+# - use_nix
+# - use_nix -s shell.nix -w .nixpkgs-version.json
+#
+# The dependencies pulled by nix-shell are added to Nix's garbage collector
+# roots, such that the environment remains persistent.
+#
+# Nix-shell is invoked only once per environment, and the output is cached for
+# better performance. If any of the watched files change, then the environment
+# is rebuilt.
+#
+# To remove old environments, and allow the GC to collect their dependencies:
+# rm -f .direnv
+#
+use_nix() {
+ if ! validate_version; then
+ echo "This .envrc requires direnv version 2.18.2 or above."
+ exit 1
+ fi
+
+ # define all local variables
+ local shell
+ local files_to_watch=()
+
+ local opt OPTARG OPTIND # define vars used by getopts locally
+ while getopts ":n:s:w:" opt; do
+ case "${opt}" in
+ s)
+ shell="${OPTARG}"
+ files_to_watch=("${files_to_watch[@]}" "${shell}")
+ ;;
+ w)
+ files_to_watch=("${files_to_watch[@]}" "${OPTARG}")
+ ;;
+ :)
+ fail "Invalid option: $OPTARG requires an argument"
+ ;;
+ \?)
+ fail "Invalid option: $OPTARG"
+ ;;
+ esac
+ done
+ shift $((OPTIND -1))
+
+ if [[ -z "${shell}" ]]; then
+ if [[ -f shell.nix ]]; then
+ shell=shell.nix
+ files_to_watch=("${files_to_watch[@]}" shell.nix)
+ elif [[ -f default.nix ]]; then
+ shell=default.nix
+ files_to_watch=("${files_to_watch[@]}" default.nix)
+ else
+ fail "ERR: no shell was given"
+ fi
+ fi
+
+ local f
+ for f in "${files_to_watch[@]}"; do
+ if ! [[ -f "${f}" ]]; then
+ fail "cannot watch file ${f} because it does not exist"
+ fi
+ done
+
+ # compute the hash of all the files that makes up the development environment
+ local env_hash="$(hash_contents "${files_to_watch[@]}")"
+
+ # define the paths
+ local dir="$(direnv_layout_dir)"
+ local wd="${dir}/wd-${env_hash}"
+ local drv="${wd}/env.drv"
+ local dump="${wd}/dump.env"
+
+ # Generate the environment if we do not have one generated already.
+ if [[ ! -f "${drv}" ]]; then
+ mkdir -p "${wd}"
+
+ log_status "use nix: deriving new environment"
+ IN_NIX_SHELL=1 nix-instantiate --add-root "${drv}" --indirect "${shell}" > /dev/null
+ nix-store -r $(nix-store --query --references "${drv}") --add-root "${wd}/dep" --indirect > /dev/null
+ if [[ "${?}" -ne 0 ]] || [[ ! -f "${drv}" ]]; then
+ rm -rf "${wd}"
+ fail "use nix: was not able to derive the new environment. Please run 'direnv reload' to try again."
+ fi
+
+ log_status "use nix: updating cache"
+ nix-shell --pure "${drv}" --show-trace --run "$(join_args "$direnv" dump bash)" > "${dump}"
+ if [[ "${?}" -ne 0 ]] || [[ ! -f "${dump}" ]] || ! grep -q IN_NIX_SHELL "${dump}"; then
+ rm -rf "${wd}"
+ fail "use nix: was not able to update the cache of the environment. Please run 'direnv reload' to try again."
+ fi
+ fi
+
+ # evaluate the dump created by nix-shell earlier, but have to merge the PATH
+ # with the current PATH
+ # NOTE: we eval the dump here as opposed to direnv_load it because we don't
+ # want to persist environment variables coming from the shell at the time of
+ # the dump. See https://github.com/direnv/direnv/issues/405 for context.
+ local path_backup="${PATH}"
+ eval $(cat "${dump}")
+ export PATH="${PATH}:${path_backup}"
+
+ # cleanup the environment of variables that are not requried, or are causing issues.
+ unset shellHook # when shellHook is present, then any nix-shell'd script will execute it!
+
+ # watch all the files we were asked to watch for the environment
+ for f in "${files_to_watch[@]}"; do
+ watch_file "${f}"
+ done
+}
+
+fail() {
+ log_error "${@}"
+ exit 1
+}
+
+hash_contents() {
+ if has md5sum; then
+ cat "${@}" | md5sum | cut -c -32
+ elif has md5; then
+ cat "${@}" | md5 -q
+ fi
+}
+
+hash_file() {
+ if has md5sum; then
+ md5sum "${@}" | cut -c -32
+ elif has md5; then
+ md5 -q "${@}"
+ fi
+}
+
+validate_version() {
+ local version="$("${direnv}" version)"
+ local major="$(echo "${version}" | cut -d. -f1)"
+ local minor="$(echo "${version}" | cut -d. -f2)"
+ local patch="$(echo "${version}" | cut -d. -f3)"
+
+ if [[ "${major}" -gt 2 ]]; then return 0; fi
+ if [[ "${major}" -eq 2 ]] && [[ "${minor}" -gt 18 ]]; then return 0; fi
+ if [[ "${major}" -eq 2 ]] && [[ "${minor}" -eq 18 ]] && [[ "${patch}" -ge 2 ]]; then return 0; fi
+ return 1
+}
+
+use_nix -s shell.nix -w .nixpkgs-version.json
diff --git a/.nixpkgs-version.json b/.nixpkgs-version.json
new file mode 100644
index 0000000..f5496a7
--- /dev/null
+++ b/.nixpkgs-version.json
@@ -0,0 +1,7 @@
+{
+ "url": "https://github.com/NixOS/nixpkgs-channels.git",
+ "rev": "3d6cdfa24bd771b36e8c87241d17275dd144c7a3",
+ "date": "2020-03-05T08:28:11+00:00",
+ "sha256": "1cif0z3zwwr2m5ibrm2b51dw8pqfqv4n95xhfiqr8zl941k18gkr",
+ "fetchSubmodules": false
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f9ab36b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,7 @@
+# EBK Notify
+
+Notify via different channels once a new item popped up on Ebay Kleinanzeigen.
+This Projekt utilizes the original code from [ebk-client](https://github.com/tejado/ebk-client/)
+
+# License
+MIT
diff --git a/ebknotify/cli.py b/ebknotify/cli.py
new file mode 100644
index 0000000..67a4e9a
--- /dev/null
+++ b/ebknotify/cli.py
@@ -0,0 +1,3 @@
+
+def main():
+ pass
diff --git a/ebknotify/client.py b/ebknotify/client.py
new file mode 100644
index 0000000..3abcb07
--- /dev/null
+++ b/ebknotify/client.py
@@ -0,0 +1,227 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+ebk-client - eBay Kleinanzeigen/Classifieds API client in Python
+Copyright (c) 2016 tjado <https://github.com/tejado>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+OR OTHER DEALINGS IN THE SOFTWARE.
+
+Author: tjado <https://github.com/tejado>
+"""
+
+import json
+import base64
+import hashlib
+import dateutil.parser
+import requests
+
+from datetime import datetime
+from dateutil.tz import tzlocal
+
+import logging
+logging.basicConfig(level=logging.DEBUG)
+
+try:
+ from html import unescape # python 3.4+
+except ImportError:
+ try:
+ from html.parser import HTMLParser # python 3.x (<3.4)
+ except ImportError:
+ from HTMLParser import HTMLParser # python 2.x
+ unescape = HTMLParser().unescape
+
+class EbkClient:
+
+ H_EBAYK_CLIENT_APP = '13a6dde3-935d-4cd8-9992-db8a8c4b6c0f1456515662228'
+ H_EBAYK_CLIENT_VERSION = '5423'
+ H_EBAYK_CLIENT_TYPE = 'ebayk-android-app-6.9.3'
+ H_EBAYK_CLIENT_UA = 'Dalvik/2.1.0'
+
+ URL_PREFIX = 'https://api.ebay-kleinanzeigen.de/api'
+
+ username = None
+
+ def __init__(self, app_username, app_password, user_username, user_password):
+
+ try:
+ import requests.packages.urllib3
+ requests.packages.urllib3.disable_warnings()
+ except:
+ pass
+
+ self.username = user_username
+ app_auth = base64.b64encode('{}:{}'.format(app_username, app_password).encode('ascii')).decode("utf-8")
+
+ hashed_user_password = base64.b64encode(hashlib.sha1(user_password.encode('ascii')).digest()).decode("utf-8")
+ user_auth = 'email="{}",password="{}"'.format(user_username, hashed_user_password)
+
+ header = {
+ 'X-EBAYK-APP': self.H_EBAYK_CLIENT_APP,
+ 'X-ECG-USER-VERSION': self.H_EBAYK_CLIENT_VERSION,
+ 'X-ECG-USER-AGENT': self.H_EBAYK_CLIENT_TYPE,
+ 'Authorization': 'Basic {}'.format(app_auth),
+ 'X-ECG-Authorization-User': user_auth,
+ 'User-Agent': self.H_EBAYK_CLIENT_UA,
+ }
+
+ self._session = requests.session()
+ self._session.headers.update(header)
+
+ def _validate_http_response(self, r):
+ if r.status_code == 401:
+ raise Exception('Access Denied (e.g. wrong app credentials or user credentials)')
+ elif r.status_code == 404:
+ raise Exception('Not found')
+ elif r.status_code == 500:
+ raise Exception('Internal Server Error (e.g. wrong request)')
+
+ def _http_get(self, url_suffix,params=None):
+ if not params: params = {}
+ response = self._session.get( self.URL_PREFIX + url_suffix,params=params )
+ self._validate_http_response(response)
+ return response
+
+ def _http_post(self, url_suffix, post_data = ''):
+ response = self._session.post( self.URL_PREFIX + url_suffix, data=post_data, headers={'Content-Type': 'application/xml'} )
+ self._validate_http_response(response)
+ return response
+
+ def _http_put(self, url_suffix, post_data = ''):
+ response = self._session.put( self.URL_PREFIX + url_suffix, data=post_data, headers={'Content-Type': 'application/xml'} )
+ self._validate_http_response(response)
+ return response
+
+ def _http_delete(self, url_suffix):
+ response = self._session.delete( self.URL_PREFIX + url_suffix )
+ self._validate_http_response(response)
+ return response
+
+
+ def get_ads(self,**params):
+ # latitude=12.42&longitude=-34.44&distance=50&distanceUnit=KM
+ # locationId=12,13
+ # zipcode=70435
+ #
+ # usage:
+ # api.get_ads(zipcode="70435",categoryId=80,distance=2,distanceUnit="KM")
+ if not params: params = {}
+ url = "/ads.json"
+ response_json = self._http_get(url,params).json()
+ ads = response_json.get('{http://www.ebayclassifiedsgroup.com/schema/ad/v1}ads', {}).get('value', {}).get('ad', None)
+ return ads
+
+ def get_my_ads(self):
+ url = "/users/{}/ads.json?_in=id,title,start-date-time,ad-status".format(self.username)
+ response = self._http_get(url)
+ response_json = json.loads(response.content.decode('utf-8'))
+ my_ads = response_json.get('{http://www.ebayclassifiedsgroup.com/schema/ad/v1}ads', {}).get('value', {}).get('ad', None)
+
+ return my_ads
+
+ def get_ad_details(self,ident):
+ url = "/ads/{}.json".format(ident)
+ response = self._http_get(url).json()
+ return response.get('{http://www.ebayclassifiedsgroup.com/schema/ad/v1}ad', {}).get('value', {})
+
+ def change_ad_status(self, ad_id, status):
+ if status not in ['active', 'paused']:
+ raise Exception('Wrong ad status')
+
+ url = "/users/{}/ads/{}/{}.json".format(self.username, status, ad_id)
+ response = self._http_put(url)
+
+ if response.status_code == 204:
+ return True
+ else:
+ return False
+
+ def activate_ad(self, ad_id):
+ return change_ad_status(ad_id, 'active')
+
+ def deactivate_ad(self, ad_id):
+ return change_ad_status(ad_id, 'paused')
+
+ def delete_ad(self, ad_id):
+ url = "/users/{}/ads/{}".format(self.username, ad_id)
+ response = self._http_delete(url)
+
+ if response.status_code == 204:
+ return True
+ else:
+ return False
+
+ def create_ad(self, xml):
+ url = "/users/{}/ads.json".format(self.username)
+ response = self._http_post(url, xml)
+ return response
+
+ def get_categories(self, cat_id = None):
+ if cat_id is not None:
+ url = "/categories/{}.json".format(cat_id)
+ schema_uri = '{http://www.ebayclassifiedsgroup.com/schema/category/v1}category'
+ else:
+ url = "/categories.json"
+ schema_uri = '{http://www.ebayclassifiedsgroup.com/schema/category/v1}categories'
+
+ response = self._http_get(url)
+ response_json = json.loads(response.content.decode('utf-8'))
+ categories = response_json.get(schema_uri, {}).get('value', {}).get('category', None)
+
+ return categories
+
+ def get_category_attributes(self, cat_id = None):
+
+ url = "/attributes/metadata/{}.json".format(cat_id)
+ schema_uri = '{http://www.ebayclassifiedsgroup.com/schema/attribute/v1}attributes'
+
+ response = self._http_get(url)
+ response_json = json.loads(response.content.decode('utf-8'))
+ cat_attr = response_json.get(schema_uri, {}).get('value', {}).get('attribute', None)
+
+ return cat_attr
+
+ def get_locations(self, url_suffix, depth = None, include_parent_path = False):
+ url = "/locations.json?{}".format(url_suffix)
+ if depth is not None:
+ url += '&depth={}'.format(depth)
+
+ if include_parent_path is True:
+ url += '&includeParentPath=true'
+
+ schema_uri = '{http://www.ebayclassifiedsgroup.com/schema/location/v1}locations'
+
+ response = self._http_get(url)
+ response_json = json.loads(response.content.decode('utf-8'))
+ categories = response_json.get(schema_uri, {}).get('value', {}).get('location', None)
+
+ return categories
+
+ def get_location_by_name(self, location_name, depth = None, include_parent_path = False):
+ url_suffix = "q={}".format(location_name)
+ locations = self.get_locations(url_suffix, depth, include_parent_path)
+ return locations
+
+ def get_location_by_coordinates(self, latitude, longitude, depth = None, include_parent_path = False):
+ url_suffix = "latitude={}&longitude={}".format(latitude,longitude)
+ locations = self.get_locations(url_suffix, depth, include_parent_path)
+ return locations
+
+ def html_unescape(self, data):
+ return unescape(data.decode())
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..e4a01da
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,26 @@
+from setuptools import setup
+
+setup(
+ name="ebknotify",
+ description="Notify on new ebay kleinanzeigen inputs",
+ version="1.0.0",
+ packages=["ebknotify"],
+ license="MIT",
+ long_description=open("README.md").read(),
+ author="Felix Richter",
+ author_email="github@krebsco.de",
+ install_requires=[
+ "requests",
+ "docopt"
+ ],
+ entry_points={"console_scripts": ["ebk-notify = ebknotify.cli:main"]},
+ classifiers=[
+ "Intended Audience :: Human",
+ "Natural Language :: English",
+ "Operating System :: POSIX :: Linux",
+ "Development Status :: 3 - Alpha",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: Implementation :: CPython",
+ ],
+)
diff --git a/shell.nix b/shell.nix
new file mode 100644
index 0000000..79beb8a
--- /dev/null
+++ b/shell.nix
@@ -0,0 +1,25 @@
+let
+ # Look here for information about how to generate `nixpkgs-version.json`.
+ # → https://nixos.wiki/wiki/FAQ/Pinning_Nixpkgs
+ pinnedVersion = builtins.fromJSON (builtins.readFile ./.nixpkgs-version.json);
+ pinnedPkgs = import (builtins.fetchGit {
+ inherit (pinnedVersion) url rev;
+
+ ref = "nixos-unstable";
+ }) {};
+in
+
+# This allows overriding pkgs by passing `--arg pkgs ...`
+{ pkgs ? pinnedPkgs }:
+
+with pkgs.python3.pkgs;
+buildPythonPackage {
+ name = "env";
+ src = ./.;
+ propagatedBuildInputs = [
+ docopt
+ requests
+ beautifulsoup4
+ ];
+ checkInputs = [ black jq ];
+}