Compare commits
14 Commits
173dcb0f50
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 85e7887b65 | |||
| a7fe9d8614 | |||
| 0bfc2fd9c5 | |||
| 51d4f30de3 | |||
| 6fe1292086 | |||
| 958000ce36 | |||
| 3a2c744ed6 | |||
|
|
83033c3c8d | ||
|
|
5d5bbb12b2 | ||
|
|
b0182c606d | ||
|
|
9a67af7480 | ||
|
|
86921652e3 | ||
|
|
5713fcb0cf | ||
|
|
bea8a41670 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -369,3 +369,5 @@ FodyWeavers.xsd
|
|||||||
/myenv
|
/myenv
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
allauctions_NL.json
|
allauctions_NL.json
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
|||||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Dockerfile
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# (optioneel: system deps toevoegen als later nodig voor DB/libs)
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Python deps
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# App code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Simpel: zelfde als startup.bat, maar dan Linux
|
||||||
|
CMD ["python", "app.py"]
|
||||||
@@ -1 +1,4 @@
|
|||||||
# PythonAuctionviewer
|
# PythonAuctionviewer
|
||||||
|
|
||||||
|
## Run dev server
|
||||||
|
To run the development server run `python app.py` in the root of the project
|
||||||
11
app.py
11
app.py
@@ -7,7 +7,7 @@ from utils.auctionutils import getAuctionlocations
|
|||||||
from models.location import JsonEncoder
|
from models.location import JsonEncoder
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app, resources={r"/*": {"origins": ["http://localhost:4200","https://auctionviewer.ikbenhenk.nl"]}})
|
CORS(app, resources={r"/*": {"origins": ["*"]}})
|
||||||
application = app # our hosting requires application in passenger_wsgi
|
application = app # our hosting requires application in passenger_wsgi
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
@@ -28,7 +28,7 @@ def getAllAuctions(countrycode):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log('something went wrong ')
|
log('something went wrong ')
|
||||||
print_exc(e)
|
print_exc()
|
||||||
return 'internal server error', 500
|
return 'internal server error', 500
|
||||||
|
|
||||||
@app.route("/v2/refreshauction/<countrycode>")
|
@app.route("/v2/refreshauction/<countrycode>")
|
||||||
@@ -43,12 +43,13 @@ def refreshAllAuctions(countrycode):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log('something went wrong with refreshing the auctions')
|
log('something went wrong with refreshing the auctions')
|
||||||
print_exc(e)
|
print_exc()
|
||||||
return 'internal server error' + e, 500
|
return 'internal server error', 500
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run() # run our Flask app
|
app.run(host="0.0.0.0", port=5000) # ipv alleen app.run()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
151
bindToInterface.c
Normal file
151
bindToInterface.c
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <net/if.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
//#define DEBUG
|
||||||
|
|
||||||
|
//compile with gcc -nostartfiles -fpic -shared bindToInterface.c -o bindToInterface.so -ldl -D_GNU_SOURCE
|
||||||
|
//Use with BIND_INTERFACE=<network interface> LD_PRELOAD=./bindInterface.so <your program> like curl ifconfig.me
|
||||||
|
|
||||||
|
int bind_to_source_ip(int sockfd, const char *source_ip)
|
||||||
|
{
|
||||||
|
struct sockaddr_in source_addr;
|
||||||
|
memset(&source_addr, 0, sizeof(source_addr));
|
||||||
|
source_addr.sin_family = AF_INET;
|
||||||
|
source_addr.sin_addr.s_addr = inet_addr(source_ip);
|
||||||
|
|
||||||
|
return bind(sockfd, (struct sockaddr *)&source_addr, sizeof(source_addr));
|
||||||
|
}
|
||||||
|
|
||||||
|
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
|
||||||
|
{
|
||||||
|
int *(*original_connect)(int, const struct sockaddr *, socklen_t);
|
||||||
|
original_connect = dlsym(RTLD_NEXT, "connect");
|
||||||
|
|
||||||
|
static struct sockaddr_in *socketAddress;
|
||||||
|
socketAddress = (struct sockaddr_in *)addr;
|
||||||
|
|
||||||
|
char *dest = inet_ntoa(socketAddress->sin_addr);
|
||||||
|
|
||||||
|
if (socketAddress->sin_family == AF_INET)
|
||||||
|
{
|
||||||
|
unsigned short port = ntohs(socketAddress->sin_port);
|
||||||
|
|
||||||
|
char *DNSIP_env = getenv("DNS_OVERRIDE_IP");
|
||||||
|
char *DNSPort_env = getenv("DNS_OVERRIDE_PORT");
|
||||||
|
int port_new = port;
|
||||||
|
|
||||||
|
if (port == 53 && DNSIP_env != NULL && strlen(DNSIP_env) > 0)
|
||||||
|
{
|
||||||
|
if (DNSPort_env != NULL && strlen(DNSPort_env) > 0)
|
||||||
|
{
|
||||||
|
port_new = atoi(DNSPort_env);
|
||||||
|
socketAddress->sin_port = htons(port_new);
|
||||||
|
}
|
||||||
|
#ifdef DEBUG
|
||||||
|
printf("Detected DNS query to: %s:%i, overwriting with %s:%i \n", dest, port, DNSIP_env, port_new);
|
||||||
|
#endif
|
||||||
|
socketAddress->sin_addr.s_addr = inet_addr(DNSIP_env);
|
||||||
|
}
|
||||||
|
port = port_new;
|
||||||
|
dest = inet_ntoa(socketAddress->sin_addr); //with #include <arpa/inet.h>
|
||||||
|
|
||||||
|
#ifdef DEBUG
|
||||||
|
printf("connecting to: %s:%i \n", dest, port);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool IPExcluded = false;
|
||||||
|
char *bind_excludes = getenv("BIND_EXCLUDE");
|
||||||
|
if (bind_excludes != NULL && strlen(bind_excludes) > 0)
|
||||||
|
{
|
||||||
|
bind_excludes = (char*) malloc(strlen(getenv("BIND_EXCLUDE")) * sizeof(char) + 1);
|
||||||
|
strcpy(bind_excludes,getenv("BIND_EXCLUDE"));
|
||||||
|
char sep[] = ",";
|
||||||
|
char *iplist;
|
||||||
|
iplist = strtok(bind_excludes, sep);
|
||||||
|
while (iplist != NULL)
|
||||||
|
{
|
||||||
|
if(!strncmp(dest,iplist,strlen(iplist)))
|
||||||
|
{
|
||||||
|
IPExcluded = true;
|
||||||
|
#ifdef DEBUG
|
||||||
|
printf("IP %s excluded by IP-List, not binding to interface %s\n", dest, getenv("BIND_INTERFACE"));
|
||||||
|
#endif
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
iplist = strtok(NULL, sep);
|
||||||
|
}
|
||||||
|
free(bind_excludes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IPExcluded) //Don't bind when destination is localhost, because it couldn't be reached anymore
|
||||||
|
{
|
||||||
|
char *bind_addr_env;
|
||||||
|
bind_addr_env = getenv("BIND_INTERFACE");
|
||||||
|
char *source_ip_env;
|
||||||
|
source_ip_env = getenv("BIND_SOURCE_IP");
|
||||||
|
struct ifreq interface;
|
||||||
|
|
||||||
|
int errorCode;
|
||||||
|
if (bind_addr_env != NULL && strlen(bind_addr_env) > 0)
|
||||||
|
{
|
||||||
|
// printf(bind_addr_env);
|
||||||
|
|
||||||
|
struct ifreq boundInterface =
|
||||||
|
{
|
||||||
|
.ifr_name = "none",
|
||||||
|
};
|
||||||
|
socklen_t optionlen = sizeof(boundInterface);
|
||||||
|
errorCode = getsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, &boundInterface, &optionlen);
|
||||||
|
if (errorCode < 0)
|
||||||
|
{
|
||||||
|
//getsockopt should not fail
|
||||||
|
perror("getsockopt");
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
#ifdef DEBUG
|
||||||
|
printf("Bound Interface: %s.", boundInterface.ifr_name);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!strcmp(boundInterface.ifr_name, "none") || strcmp(boundInterface.ifr_name, bind_addr_env))
|
||||||
|
{
|
||||||
|
#ifdef DEBUG
|
||||||
|
printf(" Socket not bound to desired interface (Bound to: %s). Binding to interface: %s\n", boundInterface.ifr_name, bind_addr_env);
|
||||||
|
#endif
|
||||||
|
strcpy(interface.ifr_name, bind_addr_env);
|
||||||
|
errorCode = setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, &interface, sizeof(interface)); //Will fail if socket is already bound to another interface
|
||||||
|
if (errorCode < 0)
|
||||||
|
{
|
||||||
|
perror("setsockopt");
|
||||||
|
errno = ENETUNREACH; //Let network appear unreachable for maximum security when desired interface is not available
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(source_ip_env != NULL && strlen(source_ip_env) > 0){
|
||||||
|
if (bind_to_source_ip(sockfd, source_ip_env) < 0){
|
||||||
|
perror("bind_to_source_ip failed");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!(source_ip_env != NULL && strlen(source_ip_env) > 0) && !(bind_addr_env != NULL && strlen(bind_addr_env) > 0)){
|
||||||
|
printf("Warning: Program with LD_PRELOAD started, but BIND_INTERFACE environment variable not set\n");
|
||||||
|
fprintf(stderr, "Warning: Program with LD_PRELOAD started, but BIND_INTERFACE environment variable not set\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (uintptr_t)original_connect(sockfd, addr, addrlen);
|
||||||
|
|
||||||
|
}
|
||||||
71
cache.py
71
cache.py
@@ -1,4 +1,5 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import time
|
import time
|
||||||
@@ -9,19 +10,20 @@ from utils.helperutils import log
|
|||||||
|
|
||||||
cache = {}
|
cache = {}
|
||||||
|
|
||||||
|
|
||||||
class Cache():
|
class Cache():
|
||||||
def get(key, notOlderThanHours = 24):
|
def get(key, notOlderThanHours=24):
|
||||||
#print('get key ' + key)
|
# print('get key ' + key)
|
||||||
if key in cache:
|
if key in cache:
|
||||||
cacheobj = cache[key]
|
cacheobj = cache[key]
|
||||||
if(not cache):
|
if (not cache):
|
||||||
return None
|
return None
|
||||||
if(cacheobj.isOlderThanHours(notOlderThanHours)):
|
if (cacheobj.isOlderThanHours(notOlderThanHours)):
|
||||||
log('removing cacheobject ' + key)
|
log('removing cacheobject ' + key)
|
||||||
del cache[key]
|
del cache[key]
|
||||||
return None
|
return None
|
||||||
# log( 'returning cacheobject ' + key)
|
# log( 'returning cacheobject ' + key)
|
||||||
return cacheobj.obj
|
return cacheobj.obj
|
||||||
|
|
||||||
def add(key, obj):
|
def add(key, obj):
|
||||||
log('adding cacheobject ' + key)
|
log('adding cacheobject ' + key)
|
||||||
@@ -33,7 +35,7 @@ class CacheObj:
|
|||||||
def __init__(self, key, obj):
|
def __init__(self, key, obj):
|
||||||
self.key = key
|
self.key = key
|
||||||
self.obj = obj
|
self.obj = obj
|
||||||
self.time=datetime.now()
|
self.time = datetime.now()
|
||||||
|
|
||||||
def isOlderThanHours(self, hours):
|
def isOlderThanHours(self, hours):
|
||||||
# log('checking time cacheobject ' + self.key + ': ' + str(self.time) + " < " + str(datetime.now() - timedelta(hours=hours)))
|
# log('checking time cacheobject ' + self.key + ': ' + str(self.time) + " < " + str(datetime.now() - timedelta(hours=hours)))
|
||||||
@@ -41,31 +43,36 @@ class CacheObj:
|
|||||||
|
|
||||||
|
|
||||||
class FileCache():
|
class FileCache():
|
||||||
def get(key, notOlderThanHours = None):
|
def get(key, notOlderThanHours=None):
|
||||||
|
|
||||||
filepath = "./filecache/" + key + ".json"
|
filepath = "./filecache/" + key + ".json"
|
||||||
cachefile = Path(filepath)
|
cachefile = Path(filepath)
|
||||||
if cachefile.is_file():
|
if cachefile.is_file():
|
||||||
ti_m = os.path.getmtime(filepath)
|
ti_m = os.path.getmtime(filepath)
|
||||||
|
|
||||||
if(notOlderThanHours is not None):
|
if (notOlderThanHours is not None):
|
||||||
#checks last modified age of file, and removes it if it is too old
|
# checks last modified age of file, and removes it if it is too old
|
||||||
log('checking time cachefile ' + filepath + ': ' + str(ti_m) + " < " + str(time.time() - (3600 * notOlderThanHours)))
|
log('checking time cachefile ' + filepath + ': ' + str(ti_m) + " < " + str(time.time() - (3600 * notOlderThanHours)))
|
||||||
|
|
||||||
if( ti_m < time.time() - (3600 * notOlderThanHours)):
|
if (ti_m < time.time() - (3600 * notOlderThanHours)):
|
||||||
log('removing old filecache')
|
log('removing old filecache')
|
||||||
os.remove(filepath)
|
os.remove(filepath)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
with open(filepath) as json_file:
|
with open(filepath) as json_file:
|
||||||
json_data = json.load(json_file)
|
json_data = json.load(json_file)
|
||||||
log('returning json data from cachefile: ' + key)
|
log('returning json data from cachefile: ' + key)
|
||||||
return json_data
|
return json_data
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def add(key, obj):
|
def add(key, obj):
|
||||||
log('adding filecacheobject ' + key)
|
log('adding filecacheobject ' + key)
|
||||||
json_data = JsonEncoder().encode(obj)
|
json_data = JsonEncoder().encode(obj)
|
||||||
|
# json_data = json.dumps(obj, cls=JsonEncoder, indent=2)
|
||||||
|
|
||||||
|
# Ensure the filecache directory exists
|
||||||
|
os.makedirs("./filecache", exist_ok=True)
|
||||||
|
|
||||||
with open("./filecache/" + key + ".json", 'w+') as f:
|
with open("./filecache/" + key + ".json", 'w+') as f:
|
||||||
f.write(json_data)
|
f.write(json_data)
|
||||||
|
|||||||
@@ -465,6 +465,7 @@
|
|||||||
2743947 Zevenbergen Zevenbergen Zevenbergen,ze fen bei heng,zefenberugen,Зевенберген,ゼーフェンベルゲン,澤芬貝亨 51.645 4.60417 P PPL NL 06 1709 0 5 Europe/Amsterdam 2017-10-17
|
2743947 Zevenbergen Zevenbergen Zevenbergen,ze fen bei heng,zefenberugen,Зевенберген,ゼーフェンベルゲン,澤芬貝亨 51.645 4.60417 P PPL NL 06 1709 0 5 Europe/Amsterdam 2017-10-17
|
||||||
2743948 Gemeente Zevenaar Gemeente Zevenaar Gemeente Zevenaar,Zevenaar 51.95376 6.07727 A ADM2 NL 03 0299 44096 9 Europe/Amsterdam 2023-01-31
|
2743948 Gemeente Zevenaar Gemeente Zevenaar Gemeente Zevenaar,Zevenaar 51.95376 6.07727 A ADM2 NL 03 0299 44096 9 Europe/Amsterdam 2023-01-31
|
||||||
2743949 Zevenaar Zevenaar Zevenar,Зевенар 51.93 6.07083 P PPL NL 03 0299 26063 12 Europe/Amsterdam 2017-10-17
|
2743949 Zevenaar Zevenaar Zevenar,Зевенар 51.93 6.07083 P PPL NL 03 0299 26063 12 Europe/Amsterdam 2017-10-17
|
||||||
|
2743949 omgeving zevenaar omgeving zevenaar Zevenar,Зевенар 51.93 6.07083 P PPL NL 03 0299 26063 12 Europe/Amsterdam 2017-10-17
|
||||||
2743950 Zeumersche Beek Zeumersche Beek 52.16452 5.53953 H STM NL 03 0 5 Europe/Amsterdam 2011-04-19
|
2743950 Zeumersche Beek Zeumersche Beek 52.16452 5.53953 H STM NL 03 0 5 Europe/Amsterdam 2011-04-19
|
||||||
2743951 Zeumeren Zeumeren 52.17256 5.60208 P PPL NL 03 0203 0 12 Europe/Amsterdam 2009-01-17
|
2743951 Zeumeren Zeumeren 52.17256 5.60208 P PPL NL 03 0203 0 12 Europe/Amsterdam 2009-01-17
|
||||||
2743952 Zetten Zetten 51.92833 5.71389 P PPL NL 03 1734 2985 10 Europe/Amsterdam 2017-10-17
|
2743952 Zetten Zetten 51.92833 5.71389 P PPL NL 03 1734 2985 10 Europe/Amsterdam 2017-10-17
|
||||||
@@ -13230,6 +13231,7 @@
|
|||||||
2757247 Den Bramel Den Bramel 52.12176 6.3111 S CSTL NL 03 1876 0 15 Europe/Amsterdam 2011-06-04
|
2757247 Den Bramel Den Bramel 52.12176 6.3111 S CSTL NL 03 1876 0 15 Europe/Amsterdam 2011-06-04
|
||||||
2757248 Den Braam Den Braam 52.1375 6.84028 P PPL NL 15 0158 0 38 Europe/Amsterdam 2007-06-03
|
2757248 Den Braam Den Braam 52.1375 6.84028 P PPL NL 15 0158 0 38 Europe/Amsterdam 2007-06-03
|
||||||
2757251 Den Bosch Den Bosch 52.07795 6.13621 S EST NL 03 0213 0 13 Europe/Amsterdam 2011-06-04
|
2757251 Den Bosch Den Bosch 52.07795 6.13621 S EST NL 03 0213 0 13 Europe/Amsterdam 2011-06-04
|
||||||
|
2757251 's hertogenbosch 's hertogenbosch 52.07795 6.13621 S EST NL 03 0213 0 13 Europe/Amsterdam 2011-06-04
|
||||||
2757252 Polder Den Bommel Polder Den Bommel 51.70597 4.28369 T PLDR NL 11 0 -2 Europe/Amsterdam 2011-04-19
|
2757252 Polder Den Bommel Polder Den Bommel 51.70597 4.28369 T PLDR NL 11 0 -2 Europe/Amsterdam 2011-04-19
|
||||||
2757253 Den Bommel Den Bommel Bommel 51.71583 4.28472 P PPL NL 11 1924 0 -1 Europe/Amsterdam 2017-10-17
|
2757253 Den Bommel Den Bommel Bommel 51.71583 4.28472 P PPL NL 11 1924 0 -1 Europe/Amsterdam 2017-10-17
|
||||||
2757254 Polder Den Bol Polder Den Bol 51.71665 4.75829 T PLDR NL 06 0 -1 Europe/Amsterdam 2011-04-19
|
2757254 Polder Den Bol Polder Den Bol 51.71665 4.75829 T PLDR NL 06 0 -1 Europe/Amsterdam 2011-04-19
|
||||||
|
|||||||
6
haha.bat
Normal file
6
haha.bat
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@echo off
|
||||||
|
:: adjust path below to your install location
|
||||||
|
call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" x64
|
||||||
|
|
||||||
|
:: compile the given file(s)
|
||||||
|
cl /EHsc /W4 /O2 %*
|
||||||
@@ -1,19 +1,25 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
import enum
|
||||||
from json import JSONEncoder
|
from json import JSONEncoder
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
class Countrycode(Enum):
|
class Countrycode(Enum):
|
||||||
NL = "NL",
|
NL = "NL",
|
||||||
DE = "DE",
|
DE = "DE",
|
||||||
BE = "BE"
|
BE = "BE"
|
||||||
|
|
||||||
|
|
||||||
class Auctionbrand(str, Enum):
|
class Auctionbrand(str, Enum):
|
||||||
NONE = "NONE",
|
NONE = "NONE",
|
||||||
TWK = "TWK"
|
TWK = "TWK"
|
||||||
OVM = "OVM"
|
OVM = "OVM"
|
||||||
|
AP = "AP"
|
||||||
|
|
||||||
|
|
||||||
class GeonameLocation:
|
class GeonameLocation:
|
||||||
def __init__(self, geonameid = 0, name = "", asciiname = "", alternatenames = [], latitude = 0, longitude = 0, countrycode:Countrycode = Countrycode.NL, modificationdate = "") :
|
def __init__(self, geonameid=0, name="", asciiname="", alternatenames=[], latitude=0, longitude=0,
|
||||||
|
countrycode: Countrycode = Countrycode.NL, modificationdate=""):
|
||||||
self.geonameid = geonameid
|
self.geonameid = geonameid
|
||||||
self.name = name
|
self.name = name
|
||||||
self.asciiname = asciiname
|
self.asciiname = asciiname
|
||||||
@@ -23,16 +29,20 @@ class GeonameLocation:
|
|||||||
self.countrycode = countrycode
|
self.countrycode = countrycode
|
||||||
self.modificationdate = modificationdate
|
self.modificationdate = modificationdate
|
||||||
|
|
||||||
|
|
||||||
class Maplocation:
|
class Maplocation:
|
||||||
def __init__(self, lat = 0, long = 0, numberofauctions = 0, geonamelocation:GeonameLocation = None, auctions = []):
|
def __init__(self, lat=0, long=0, numberofauctions=0, geonamelocation: GeonameLocation = None, auctions=[]):
|
||||||
self.lat = lat
|
self.lat = lat
|
||||||
self.long = long
|
self.long = long
|
||||||
self.numberofauctions = numberofauctions
|
self.numberofauctions = numberofauctions
|
||||||
self.geonamelocation = geonamelocation
|
self.geonamelocation = geonamelocation
|
||||||
self.auctions = auctions
|
self.auctions = auctions
|
||||||
|
|
||||||
|
|
||||||
class Auction:
|
class Auction:
|
||||||
def __init__(self, auctionbrand: Auctionbrand = Auctionbrand.NONE, city = "", countrycode:Countrycode = Countrycode.NL, name = "", starttime = None, closingtime = None, url = "", imageurl = "", numberoflots = 0, geonamelocation: GeonameLocation = None):
|
def __init__(self, auctionbrand: Auctionbrand = Auctionbrand.NONE, city="", countrycode: Countrycode = Countrycode.NL, name="",
|
||||||
|
starttime=None, closingtime=None, url="", imageurl="", numberoflots=0, geonamelocation: GeonameLocation = None,
|
||||||
|
multiplelocations=False):
|
||||||
self.city = city
|
self.city = city
|
||||||
self.countrycode = countrycode
|
self.countrycode = countrycode
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -43,7 +53,35 @@ class Auction:
|
|||||||
self.numberoflots = numberoflots
|
self.numberoflots = numberoflots
|
||||||
self.geonamelocation = geonamelocation
|
self.geonamelocation = geonamelocation
|
||||||
self.brand = auctionbrand
|
self.brand = auctionbrand
|
||||||
|
self.multiplelocations = multiplelocations
|
||||||
|
|
||||||
|
|
||||||
class JsonEncoder(JSONEncoder):
|
class JsonEncoder(JSONEncoder):
|
||||||
def default(self, o):
|
# def default(self, o):
|
||||||
return o.__dict__
|
# return o.__dict__
|
||||||
|
# try 2
|
||||||
|
def default(self, obj):
|
||||||
|
# Only serialize public, instance-level attributes
|
||||||
|
if hasattr(obj, '__dict__'):
|
||||||
|
return {
|
||||||
|
key: self.serialize(value)
|
||||||
|
for key, value in obj.__dict__.items()
|
||||||
|
if not key.startswith('_') # skip private/protected
|
||||||
|
}
|
||||||
|
return super().default(obj)
|
||||||
|
|
||||||
|
def serialize(self, value):
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [self.serialize(item) for item in value]
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
return {k: self.serialize(v) for k, v in value.items()}
|
||||||
|
elif isinstance(value, enum.Enum):
|
||||||
|
return value.name # or value.value
|
||||||
|
elif hasattr(value, '__dict__'):
|
||||||
|
return self.default(value) # dive into nested object
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
json.dumps(value)
|
||||||
|
return value
|
||||||
|
except (TypeError, OverflowError):
|
||||||
|
return str(value)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from app import app as application
|
from app import app as application
|
||||||
|
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
|
||||||
spec = importlib.util.spec_from_file_location("wsgi", "app.py")
|
spec = importlib.util.spec_from_file_location("wsgi", "app.py")
|
||||||
wsgi = importlib.util.module_from_spec(spec)
|
wsgi = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(wsgi)
|
spec.loader.exec_module(wsgi)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
pip==9.0.1
|
pip>=23.3
|
||||||
setuptools==28.8.0
|
setuptools>=78.1.1
|
||||||
flask===3.0.3
|
flask===3.1.1
|
||||||
werkzeug==3.0.0
|
werkzeug>=3.0.6
|
||||||
flask_cors===5.0.0
|
flask_cors>=6.0.0
|
||||||
requests===2.27.1
|
requests>=2.32.4
|
||||||
pathlib===1.0.1
|
pathlib===1.0.1
|
||||||
@@ -11,6 +11,6 @@ import requests
|
|||||||
|
|
||||||
response = requests.get('https://api.auctionviewer.ikbenhenk.nl//v2/refreshauction/NL')
|
response = requests.get('https://api.auctionviewer.ikbenhenk.nl//v2/refreshauction/NL')
|
||||||
if(response.status_code ==200):
|
if(response.status_code ==200):
|
||||||
print('ran getauctions request successfull')
|
print('ran getauctions request successful')
|
||||||
else:
|
else:
|
||||||
print('A error occurred while running the getauctions request')
|
print('A error occurred while running the getauctions request')
|
||||||
26
startup.bat
Normal file
26
startup.bat
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
@echo off
|
||||||
|
echo ====================================
|
||||||
|
echo Auctionviewer API Startup Script
|
||||||
|
echo ====================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo [1/3] Installing dependencies...
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo [2/3] Starting API server...
|
||||||
|
start /B python app.py
|
||||||
|
timeout /t 3 /nobreak >nul
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo [3/3] Fetching auctions...
|
||||||
|
echo.
|
||||||
|
echo --- NL Auctions ---
|
||||||
|
curl http://localhost:5000/v2/auction/NL
|
||||||
|
echo.
|
||||||
|
echo.
|
||||||
|
echo ====================================
|
||||||
|
echo Startup complete!
|
||||||
|
echo API running at http://localhost:5000
|
||||||
|
echo ====================================
|
||||||
79
utils/APutils.py
Normal file
79
utils/APutils.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from time import ctime
|
||||||
|
from traceback import print_exc
|
||||||
|
import requests
|
||||||
|
from cache import Cache
|
||||||
|
from models.location import Auction, Auctionbrand, Countrycode, JsonEncoder
|
||||||
|
from utils.helperutils import log
|
||||||
|
|
||||||
|
|
||||||
|
def getAPAuctions():
|
||||||
|
cachename = 'AuctionPort_'
|
||||||
|
res = Cache.get(cachename)
|
||||||
|
if (res):
|
||||||
|
return res
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get("https://api.auctionport.be/auctions/small?size=100&page=1")
|
||||||
|
except:
|
||||||
|
log("The Auctionport auctions call threw a error")
|
||||||
|
|
||||||
|
if (response is None):
|
||||||
|
return []
|
||||||
|
|
||||||
|
if (response.status_code == 200):
|
||||||
|
log('Got AP Auctions')
|
||||||
|
try:
|
||||||
|
data = response.json()
|
||||||
|
pages = data['pages']
|
||||||
|
auctions = []
|
||||||
|
for i in range(0, pages - 1, 1):
|
||||||
|
log("getting page " + str(i) + ' of ' + str(pages))
|
||||||
|
# if(i > 1):
|
||||||
|
response = requests.get("https://api.auctionport.be/auctions/small?size=100&page=" + str(i))
|
||||||
|
if (response is None): continue
|
||||||
|
if (response.status_code != 200): continue
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
for PAauction in data['data']:
|
||||||
|
# makes sure that the locations array is filled with at least one location
|
||||||
|
if PAauction['locations'] == []:
|
||||||
|
PAauction['locations'] = [PAauction['location']]
|
||||||
|
|
||||||
|
# makes sure that the auction is still active
|
||||||
|
closingdate = datetime.fromisoformat(PAauction['closingDate'])
|
||||||
|
if (closingdate.date() < datetime.now().date()): continue
|
||||||
|
if (PAauction['lotCount'] <= 0): continue
|
||||||
|
|
||||||
|
multipleLocations = len(PAauction['locations']) > 1
|
||||||
|
|
||||||
|
for location in PAauction['locations']:
|
||||||
|
if not location.endswith('Nederland'): continue
|
||||||
|
loc = re.sub('Nederland', '', location)
|
||||||
|
# loc = location.split(",")
|
||||||
|
postalcodeRegex = r'(.*?)[1-9][0-9]{3} ?(?!sa|sd|ss)[a-zA-Z]{2}'
|
||||||
|
city = re.sub(postalcodeRegex, '', loc) # removes postalcode and everything in front of it
|
||||||
|
city = city.strip() # removes whitespace
|
||||||
|
city = city.strip(',') # removes trailing and leading ,
|
||||||
|
city = city.split(',') # splits on , to overcome not matching regex
|
||||||
|
city = city[len(city) - 1]
|
||||||
|
city = city.strip()
|
||||||
|
newauction = Auction(Auctionbrand.AP, city, Countrycode.NL, PAauction['title'],
|
||||||
|
datetime.fromisoformat(PAauction['openDate']),
|
||||||
|
datetime.fromisoformat(PAauction['closingDate']), 'https://www.auctionport.be/auction/' + str(PAauction['id']),
|
||||||
|
PAauction['imageUrl'], PAauction['lotCount'], None, multipleLocations)
|
||||||
|
|
||||||
|
auctions.append(newauction)
|
||||||
|
Cache.add(cachename, auctions)
|
||||||
|
return auctions
|
||||||
|
except Exception as e:
|
||||||
|
log(e.__cause__ + '-- Something went wrong in the mapping of AP auctions to auctionviewer objects. The reason was: ' + response.reason + '. The response was: ' + JsonEncoder().encode(
|
||||||
|
response.json()))
|
||||||
|
print_exc(e)
|
||||||
|
|
||||||
|
else:
|
||||||
|
log("The AP auctions call didn't gave a 200 response but a " + str(response.status_code) + ". With the reason: " + response.reason)
|
||||||
|
return []
|
||||||
55
utils/OVMutils.py
Normal file
55
utils/OVMutils.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from traceback import print_exc
|
||||||
|
import requests
|
||||||
|
from cache import Cache
|
||||||
|
from models.location import Auction, Auctionbrand, JsonEncoder
|
||||||
|
from utils.helperutils import log
|
||||||
|
|
||||||
|
|
||||||
|
def getOVMAuctions():
|
||||||
|
cachename = 'OnlineVeiling_'
|
||||||
|
res = Cache.get(cachename)
|
||||||
|
if (res):
|
||||||
|
return res
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get("https://onlineveilingmeester.nl/rest/nl/veilingen?status=open&domein=ONLINEVEILINGMEESTER")
|
||||||
|
except:
|
||||||
|
log("The OVM auctions call threw a error")
|
||||||
|
|
||||||
|
if (response is None):
|
||||||
|
return []
|
||||||
|
|
||||||
|
if (response.status_code == 200):
|
||||||
|
log('Got Ovm Auctions')
|
||||||
|
try:
|
||||||
|
data = response.json()
|
||||||
|
auctions = []
|
||||||
|
for result in data['veilingen']:
|
||||||
|
cityname = "Nederland" if result['isBezorgVeiling'] else result['afgifteAdres']['plaats']
|
||||||
|
cityname = "Nederland" if cityname is None else cityname # there can be auctions where you have to make an appointment to retrieve the lots
|
||||||
|
startdatetime = result['openingsDatumISO'].replace("T", " ").replace("Z", "")
|
||||||
|
enddatetime = result['sluitingsDatumISO'].replace("T", " ").replace("Z", "")
|
||||||
|
image = ""
|
||||||
|
# if hasattr(result, 'image') : #result['image'] :
|
||||||
|
image = result.get('image', "") # ['image']
|
||||||
|
if image == "":
|
||||||
|
images = result.get('imageList')
|
||||||
|
if (len(images) > 0):
|
||||||
|
image = images[0]
|
||||||
|
else:
|
||||||
|
log("No image found for OVM auction: " + result['naam'])
|
||||||
|
|
||||||
|
a = Auction(Auctionbrand.OVM, cityname, result['land'], result['naam'], startdatetime, enddatetime,
|
||||||
|
'https://onlineveilingmeester.nl/' + str(result['land']).lower() + '/veilingen/' + str(result['id']) + '/kavels', 'https://onlineveilingmeester.nl/images/150x150/' + image,
|
||||||
|
result['totaalKavels'])
|
||||||
|
auctions.append(a)
|
||||||
|
Cache.add(cachename, auctions)
|
||||||
|
return auctions
|
||||||
|
except Exception as e:
|
||||||
|
log(e.__cause__ + '-- Something went wrong in the mapping of OVM auctions to auctionviewer objects. The reason was: ' + response.reason + '. The response was: ' + JsonEncoder().encode(
|
||||||
|
response.json()))
|
||||||
|
print_exc(e)
|
||||||
|
|
||||||
|
else:
|
||||||
|
log("The OVM auctions call didn't gave a 200 response but a " + str(response.status_code) + ". With the reason: " + response.reason)
|
||||||
|
return []
|
||||||
83
utils/TWKutils.py
Normal file
83
utils/TWKutils.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from cache import Cache
|
||||||
|
from models.location import Auction, Auctionbrand
|
||||||
|
from utils.helperutils import log
|
||||||
|
|
||||||
|
|
||||||
|
def getTWKUrl():
|
||||||
|
response = requests.get('https://www.troostwijkauctions.com/')
|
||||||
|
if (response.status_code == 200):
|
||||||
|
buildid = re.search(r'"buildId":"([^"]*)', response.text, re.MULTILINE)
|
||||||
|
twkDataUrl = 'https://www.troostwijkauctions.com/_next/data/' + str(buildid[1]) + '/nl/'
|
||||||
|
log('buildid: ' + str(buildid[1]))
|
||||||
|
log('twkDataUrl: ' + twkDataUrl)
|
||||||
|
return twkDataUrl
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def getTwkAuctions(countrycode):
|
||||||
|
cachename = 'TwkAuctions_' + countrycode
|
||||||
|
res = Cache.get(cachename)
|
||||||
|
if (res):
|
||||||
|
return res
|
||||||
|
|
||||||
|
# buildidresponse = requests.get('https://www.troostwijkauctions.com/')
|
||||||
|
twkDataUrl = getTWKUrl()
|
||||||
|
|
||||||
|
if (twkDataUrl is None):
|
||||||
|
return []
|
||||||
|
|
||||||
|
response = requests.get(twkDataUrl + "auctions.json?countries=" + countrycode)
|
||||||
|
|
||||||
|
if (response.status_code == 200):
|
||||||
|
log('Got Twk Auctions')
|
||||||
|
data = response.json()
|
||||||
|
auctions = []
|
||||||
|
|
||||||
|
totalAuctionCount = data['pageProps']['totalSize'];
|
||||||
|
pages = math.ceil(totalAuctionCount / data['pageProps']['pageSize'])
|
||||||
|
# for result in data['pageProps']['auctionList']:
|
||||||
|
|
||||||
|
for i in range(1, pages, 1):
|
||||||
|
log("getting page " + str(i) + ' of ' + str(pages))
|
||||||
|
if (i > 1):
|
||||||
|
response = requests.get(twkDataUrl + "auctions.json?countries=" + countrycode + "&page=" + str(i))
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
for twka in data['pageProps']['listData']:
|
||||||
|
# print(twka['urlSlug'])
|
||||||
|
auctionlocations = getTWKAuction(twka)
|
||||||
|
for auction in auctionlocations:
|
||||||
|
auctions.append(auction)
|
||||||
|
|
||||||
|
Cache.add(cachename, auctions)
|
||||||
|
|
||||||
|
return auctions
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def getTWKAuction(twka):
|
||||||
|
locations = []
|
||||||
|
cities = []
|
||||||
|
|
||||||
|
if (len(twka['collectionDays']) > 0):
|
||||||
|
cities = twka['collectionDays']
|
||||||
|
elif (len(twka['deliveryCountries']) > 0):
|
||||||
|
cities = [{'countryCode': 'nl', 'city': 'Nederland'}]
|
||||||
|
|
||||||
|
image = ''
|
||||||
|
if (len(twka['images']) > 0 and twka['images'][0]['url']):
|
||||||
|
image = twka['images'][0]['url']
|
||||||
|
|
||||||
|
for city in cities:
|
||||||
|
if (city['countryCode'].upper() != 'NL'): continue
|
||||||
|
a = Auction(Auctionbrand.TWK, city['city'], city['countryCode'].upper(), twka['name'], datetime.fromtimestamp(twka['startDate']),
|
||||||
|
datetime.fromtimestamp(twka['minEndDate']), 'https://www.troostwijkauctions.com/a/' + twka['urlSlug'], image, twka['lotCount'], None, len(cities) > 1)
|
||||||
|
locations.append(a)
|
||||||
|
|
||||||
|
return locations
|
||||||
481
utils/__auction_items.py
Normal file
481
utils/__auction_items.py
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
"""
|
||||||
|
Utility functions to fetch auction items (lots) from different providers.
|
||||||
|
|
||||||
|
This module defines three functions that make HTTP calls to the public APIs of
|
||||||
|
Troostwijk Auctions (TWK), AuctionPort (AP) and Online Veilingmeester (OVM)
|
||||||
|
and normalises their responses into Python dictionaries. Each function
|
||||||
|
returns a list of dictionaries where each dictionary represents an
|
||||||
|
individual lot and includes standardised keys: ``title``, ``description``,
|
||||||
|
``bids`` (the number of bids if available), ``current_bid`` (current price
|
||||||
|
and currency if available), ``image_url`` and ``end_time``.
|
||||||
|
|
||||||
|
The implementations rely on the ``requests`` library for HTTP transport
|
||||||
|
and include basic error handling. They raise ``requests.HTTPError``
|
||||||
|
when the remote server responds with a non‑200 status code.
|
||||||
|
|
||||||
|
Note: the APIs these functions call are subject to change. Endpoints and
|
||||||
|
field names may differ depending on the auction status or provider version.
|
||||||
|
These functions are intended as a starting point for integrating with
|
||||||
|
multiple auction platforms; you may need to adjust query parameters,
|
||||||
|
header values or JSON field names if the provider updates their API.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
```
|
||||||
|
from auction_items import get_items_twk, get_items_ap, get_items_ovm
|
||||||
|
|
||||||
|
# Troostwijk Auctions (TWK): pass the visible auction identifier
|
||||||
|
lots = get_items_twk(display_id="35563")
|
||||||
|
for lot in lots:
|
||||||
|
print(lot['lot_number'], lot['title'], lot['current_bid'])
|
||||||
|
|
||||||
|
# AuctionPort (AP): pass the auction ID from the AuctionPort website
|
||||||
|
ap_lots = get_items_ap(auction_id=1323)
|
||||||
|
|
||||||
|
# Online Veilingmeester (OVM): the country code is required to build the
|
||||||
|
# endpoint path (e.g. ``'nederland'`` or ``'belgie'``) along with the
|
||||||
|
# numeric auction ID.
|
||||||
|
ovm_lots = get_items_ovm(country="nederland", auction_id=7713)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def get_items_twk(
|
||||||
|
display_id: str,
|
||||||
|
*,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 200,
|
||||||
|
locale: str = "nl",
|
||||||
|
platform: str = "WEB",
|
||||||
|
request_session: Optional[requests.Session] = None,
|
||||||
|
) -> List[Dict[str, Optional[str]]]:
|
||||||
|
"""Fetch lots (items) for a Troostwijk auction using the GraphQL API.
|
||||||
|
|
||||||
|
Troostwijk Auctions exposes its public data through a GraphQL endpoint at
|
||||||
|
``https://storefront.tbauctions.com/storefront/graphql``. The
|
||||||
|
``auctionWithLotsV5`` query returns a list of lots for a given auction.
|
||||||
|
According to the GraphQL documentation, the query accepts a
|
||||||
|
``request`` object of type ``AuctionWithLotsInputV3`` and a
|
||||||
|
``platform`` argument. The ``request`` object requires the auction's
|
||||||
|
``displayId`` (a string identifier visible in the URL of the auction
|
||||||
|
page), ``locale`` (language code), ``pageNumber``, ``pageSize`` and
|
||||||
|
two lists for range and value facets. The return type
|
||||||
|
``AuctionWithListingLots`` contains an ``auction`` and a list of
|
||||||
|
``lots`` with details such as the lot number, title, description,
|
||||||
|
current bid and images【561575328263299†screenshot】. Fields included in
|
||||||
|
this function's query correspond to those documented in the schema.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
display_id: str
|
||||||
|
The human‑readable identifier of the auction (e.g. ``"35563"``).
|
||||||
|
page: int, optional
|
||||||
|
The page number of results (defaults to 1). The API uses
|
||||||
|
1‑based page numbering. A page size of 200 appears sufficient
|
||||||
|
for most auctions.
|
||||||
|
page_size: int, optional
|
||||||
|
The maximum number of lots to fetch per page (defaults to 200).
|
||||||
|
locale: str, optional
|
||||||
|
Language/locale code for the content (defaults to ``"nl"``).
|
||||||
|
platform: str, optional
|
||||||
|
Platform enumeration value required by the API (default
|
||||||
|
``"WEB"``). Other values may include ``"B2B"`` or ``"B2C"``;
|
||||||
|
consult the GraphQL documentation if you encounter an error.
|
||||||
|
request_session: Optional[requests.Session], optional
|
||||||
|
An existing requests session to reuse connections. If omitted,
|
||||||
|
a temporary session is created for this call.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List[Dict[str, Optional[str]]]
|
||||||
|
A list of dictionaries. Each dictionary represents a lot and
|
||||||
|
contains the keys ``lot_number``, ``title``, ``description``,
|
||||||
|
``bids`` (number of bids, if provided), ``current_bid`` (a
|
||||||
|
dictionary with ``amount`` and ``currency`` or ``None`` if no bid),
|
||||||
|
``image_url`` (first image) and ``end_time`` (auction end time in
|
||||||
|
ISO 8601 format).
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
requests.HTTPError
|
||||||
|
If the HTTP response has a non‑200 status code.
|
||||||
|
Exception
|
||||||
|
For other errors such as JSON decoding failures.
|
||||||
|
"""
|
||||||
|
session = request_session or requests.Session()
|
||||||
|
url = "https://storefront.tbauctions.com/storefront/graphql"
|
||||||
|
# GraphQL query string. The fields selected here mirror those
|
||||||
|
# described in the GraphQL documentation for the ``auctionWithLotsV5``
|
||||||
|
# operation【561575328263299†screenshot】. Additional fields can be added
|
||||||
|
# if necessary.
|
||||||
|
graphql_query = """
|
||||||
|
query AuctionWithLots($request: AuctionWithLotsInputV3!, $platform: Platform!) {
|
||||||
|
auctionWithLotsV5(request: $request, platform: $platform) {
|
||||||
|
lots {
|
||||||
|
lotNumber
|
||||||
|
id
|
||||||
|
title
|
||||||
|
description
|
||||||
|
numberOfBids
|
||||||
|
currentBid {
|
||||||
|
amount
|
||||||
|
currency
|
||||||
|
}
|
||||||
|
endDateISO
|
||||||
|
images {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
# Build the variables for the query. The request object must include
|
||||||
|
# ``displayId``, ``locale``, ``pageNumber``, ``pageSize``, and two empty
|
||||||
|
# lists for range and value facets as required by the schema【835513158978214†screenshot】.
|
||||||
|
variables = {
|
||||||
|
"request": {
|
||||||
|
"displayId": str(display_id),
|
||||||
|
"locale": locale,
|
||||||
|
"pageNumber": page,
|
||||||
|
"pageSize": page_size,
|
||||||
|
# These facets are optional; empty lists mean no filters
|
||||||
|
"rangeFacetInputs": [],
|
||||||
|
"valueFacetInputs": [],
|
||||||
|
},
|
||||||
|
"platform": platform,
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
# A typical browser may send JSON content; set an Accept header
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
# The GraphQL service uses a CSRF protection token; a random
|
||||||
|
# ``x-csrf-token`` header can be supplied if needed. Leaving it
|
||||||
|
# empty usually works for public queries.
|
||||||
|
"x-csrf-token": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = session.post(
|
||||||
|
url,
|
||||||
|
json={"query": graphql_query, "variables": variables},
|
||||||
|
headers=headers,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Raise an HTTPError for non‑200 responses
|
||||||
|
try:
|
||||||
|
response.raise_for_status()
|
||||||
|
except requests.HTTPError:
|
||||||
|
logger.error("Troostwijk API returned status %s: %s", response.status_code, response.text)
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Parse the JSON body
|
||||||
|
data = response.json()
|
||||||
|
# Check for GraphQL errors
|
||||||
|
if "errors" in data and data["errors"]:
|
||||||
|
message = data["errors"]
|
||||||
|
logger.error("GraphQL returned errors: %s", message)
|
||||||
|
raise Exception(f"GraphQL returned errors: {message}")
|
||||||
|
|
||||||
|
lots = []
|
||||||
|
# Navigate the nested structure to the list of lots. The path
|
||||||
|
# matches the GraphQL selection set defined above.
|
||||||
|
try:
|
||||||
|
lot_items = data["data"]["auctionWithLotsV5"]["lots"]
|
||||||
|
except (KeyError, TypeError) as e:
|
||||||
|
logger.error("Unexpected response structure from Troostwijk API: %s", data)
|
||||||
|
raise Exception(f"Unexpected response structure: {e}")
|
||||||
|
|
||||||
|
for item in lot_items:
|
||||||
|
# Some fields may be missing; use dict.get with defaults
|
||||||
|
lot_number = item.get("lotNumber")
|
||||||
|
title = item.get("title")
|
||||||
|
description = item.get("description")
|
||||||
|
bids = item.get("numberOfBids")
|
||||||
|
current_bid = item.get("currentBid")
|
||||||
|
end_time = item.get("endDateISO")
|
||||||
|
images = item.get("images", []) or []
|
||||||
|
image_url = images[0]["url"] if images else None
|
||||||
|
lots.append(
|
||||||
|
{
|
||||||
|
"lot_number": lot_number,
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"bids": bids,
|
||||||
|
"current_bid": current_bid,
|
||||||
|
"image_url": image_url,
|
||||||
|
"end_time": end_time,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return lots
|
||||||
|
|
||||||
|
|
||||||
|
def get_items_ap(
|
||||||
|
auction_id: int,
|
||||||
|
*,
|
||||||
|
request_session: Optional[requests.Session] = None,
|
||||||
|
) -> List[Dict[str, Optional[str]]]:
|
||||||
|
"""Retrieve items (lots) from an AuctionPort auction.
|
||||||
|
|
||||||
|
AuctionPort operates a JSON API on ``https://api.auctionport.be``. While
|
||||||
|
official documentation for the lot endpoints is scarce, community code
|
||||||
|
suggests that auctions can be fetched via ``/auctions/small``【461010206788258†L10-L39】. The
|
||||||
|
corresponding lot information appears to reside under an
|
||||||
|
``/auctions/{id}/lots`` or ``/lots?auctionId={id}`` endpoint (the
|
||||||
|
platform uses XML internally for some pages as observed when visiting
|
||||||
|
``/auctions/{id}/lots`` in a browser). This function attempts to call
|
||||||
|
these endpoints in order and parse their JSON responses. If the
|
||||||
|
response is not JSON, it falls back to a simple text scrape looking
|
||||||
|
for lot numbers, titles, descriptions and current bid amounts.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
auction_id: int
|
||||||
|
The numeric identifier of the auction on AuctionPort.
|
||||||
|
request_session: Optional[requests.Session], optional
|
||||||
|
An existing requests session.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List[Dict[str, Optional[str]]]
|
||||||
|
A list of lot dictionaries with the keys ``lot_number``, ``title``,
|
||||||
|
``description``, ``bids`` (if available), ``current_bid`` (amount and
|
||||||
|
currency if provided), ``image_url`` and ``end_time``. If no lots
|
||||||
|
could be parsed, an empty list is returned.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
requests.HTTPError
|
||||||
|
If both endpoint attempts return non‑200 responses.
|
||||||
|
"""
|
||||||
|
session = request_session or requests.Session()
|
||||||
|
# Candidate endpoints for AuctionPort lots. The first URL follows the
|
||||||
|
# pattern used by the AuctionPort website; the second is a query by
|
||||||
|
# parameter. Additional endpoints can be added if discovered.
|
||||||
|
url_candidates = [
|
||||||
|
f"https://api.auctionport.be/auctions/{auction_id}/lots",
|
||||||
|
f"https://api.auctionport.be/lots?auctionId={auction_id}",
|
||||||
|
]
|
||||||
|
last_error: Optional[Exception] = None
|
||||||
|
for url in url_candidates:
|
||||||
|
try:
|
||||||
|
response = session.get(url, headers={"Accept": "application/json"}, timeout=30)
|
||||||
|
except Exception as exc:
|
||||||
|
# Capture connection errors and continue with the next endpoint
|
||||||
|
last_error = exc
|
||||||
|
continue
|
||||||
|
if response.status_code == 404:
|
||||||
|
# Try the next candidate
|
||||||
|
continue
|
||||||
|
if response.status_code >= 400:
|
||||||
|
last_error = requests.HTTPError(
|
||||||
|
f"AuctionPort API error {response.status_code} for {url}",
|
||||||
|
response=response,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
# If the response is OK, attempt to parse JSON
|
||||||
|
try:
|
||||||
|
data = response.json()
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# Not JSON: fallback to naive parsing of plain text/XML. AuctionPort
|
||||||
|
# sometimes returns XML for lots pages. We'll attempt to extract
|
||||||
|
# structured information using simple patterns.
|
||||||
|
text = response.text
|
||||||
|
lots: List[Dict[str, Optional[str]]] = []
|
||||||
|
# Split by <div class="lot"> like markers (not guaranteed). In the
|
||||||
|
# absence of a stable API specification, heuristics must be used.
|
||||||
|
# Here we use a very simple split on "Lot " followed by a number.
|
||||||
|
import re
|
||||||
|
|
||||||
|
pattern = re.compile(r"\bLot\s+(\d+)\b", re.IGNORECASE)
|
||||||
|
for match in pattern.finditer(text):
|
||||||
|
lot_number = match.group(1)
|
||||||
|
# Attempt to extract the title and description following the
|
||||||
|
# lot number. This heuristic looks for a line break or
|
||||||
|
# sentence after the lot label; adjust as necessary.
|
||||||
|
start = match.end()
|
||||||
|
segment = text[start:start + 300] # arbitrary slice length
|
||||||
|
# Title is the first sentence or line
|
||||||
|
title_match = re.search(r"[:\-]\s*(.*?)\.(?=\s|<)", segment)
|
||||||
|
title = title_match.group(1).strip() if title_match else segment.strip()
|
||||||
|
lots.append({
|
||||||
|
"lot_number": lot_number,
|
||||||
|
"title": title,
|
||||||
|
"description": None,
|
||||||
|
"bids": None,
|
||||||
|
"current_bid": None,
|
||||||
|
"image_url": None,
|
||||||
|
"end_time": None,
|
||||||
|
})
|
||||||
|
if lots:
|
||||||
|
return lots
|
||||||
|
else:
|
||||||
|
# If no lots were extracted, continue to the next candidate
|
||||||
|
last_error = Exception("Unable to parse AuctionPort lots from non‑JSON response")
|
||||||
|
continue
|
||||||
|
# If JSON parsing succeeded, inspect the structure. Some endpoints
|
||||||
|
# return a top‑level object with a ``data`` field containing a list.
|
||||||
|
lots: List[Dict[str, Optional[str]]] = []
|
||||||
|
# Attempt to locate the list of lots: it might be in ``data``, ``lots`` or
|
||||||
|
# another property.
|
||||||
|
candidate_keys = ["lots", "data", "items"]
|
||||||
|
lot_list: Optional[List[Dict[str, any]]] = None
|
||||||
|
for key in candidate_keys:
|
||||||
|
if isinstance(data, dict) and isinstance(data.get(key), list):
|
||||||
|
lot_list = data[key]
|
||||||
|
break
|
||||||
|
# If the response is a list itself (not a dict), treat it as the lot list
|
||||||
|
if lot_list is None and isinstance(data, list):
|
||||||
|
lot_list = data
|
||||||
|
if lot_list is None:
|
||||||
|
# Unknown structure; return empty list
|
||||||
|
return []
|
||||||
|
for item in lot_list:
|
||||||
|
# Map fields according to common names; adjust if your endpoint
|
||||||
|
# uses different property names. Use dict.get to avoid KeyError.
|
||||||
|
lot_number = item.get("lotNumber") or item.get("lotnumber") or item.get("id")
|
||||||
|
title = item.get("title") or item.get("naam")
|
||||||
|
description = item.get("description") or item.get("beschrijving")
|
||||||
|
bids = item.get("numberOfBids") or item.get("bidCount")
|
||||||
|
# Determine current bid: AuctionPort might provide ``currentBid`` or
|
||||||
|
# ``currentPrice`` as an object or numeric value.
|
||||||
|
current_bid_obj = item.get("currentBid") or item.get("currentPrice")
|
||||||
|
current_bid: Optional[Dict[str, any]] = None
|
||||||
|
if isinstance(current_bid_obj, dict):
|
||||||
|
current_bid = {
|
||||||
|
"amount": current_bid_obj.get("amount"),
|
||||||
|
"currency": current_bid_obj.get("currency"),
|
||||||
|
}
|
||||||
|
elif current_bid_obj is not None:
|
||||||
|
current_bid = {"amount": current_bid_obj, "currency": None}
|
||||||
|
# Image
|
||||||
|
image_url = None
|
||||||
|
if isinstance(item.get("images"), list) and item["images"]:
|
||||||
|
image_url = item["images"][0].get("url")
|
||||||
|
elif isinstance(item.get("image"), str):
|
||||||
|
image_url = item.get("image")
|
||||||
|
# End time
|
||||||
|
end_time = item.get("endDateISO") or item.get("closingDateISO") or item.get("closingDate")
|
||||||
|
lots.append(
|
||||||
|
{
|
||||||
|
"lot_number": lot_number,
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"bids": bids,
|
||||||
|
"current_bid": current_bid,
|
||||||
|
"image_url": image_url,
|
||||||
|
"end_time": end_time,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return lots
|
||||||
|
|
||||||
|
# All candidates failed
|
||||||
|
if last_error:
|
||||||
|
raise last_error
|
||||||
|
raise requests.HTTPError(f"Could not fetch lots for AuctionPort auction {auction_id}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_items_ovm(
|
||||||
|
country: str,
|
||||||
|
auction_id: int,
|
||||||
|
*,
|
||||||
|
request_session: Optional[requests.Session] = None,
|
||||||
|
) -> List[Dict[str, Optional[str]]]:
|
||||||
|
"""Fetch lots from an Online Veilingmeester auction.
|
||||||
|
|
||||||
|
Online Veilingmeester's REST API exposes auctions and their lots via
|
||||||
|
endpoints under ``https://onlineveilingmeester.nl/rest/nl``. The
|
||||||
|
AuctionViewer project's source code constructs lot URLs as
|
||||||
|
``{land}/veilingen/{id}/kavels``, where ``land`` is the lower‑cased
|
||||||
|
country name (e.g. ``nederland`` or ``belgie``)【366543684390870†L13-L50】.
|
||||||
|
Therefore, the full path for retrieving the lots of a specific auction
|
||||||
|
is ``https://onlineveilingmeester.nl/rest/nl/{country}/veilingen/{auction_id}/kavels``.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
country: str
|
||||||
|
Lower‑case country name used in the API path (for example
|
||||||
|
``"nederland"`` or ``"belgie"``). The value should correspond to
|
||||||
|
the ``land`` property returned by the OVM auctions endpoint【366543684390870†L13-L50】.
|
||||||
|
auction_id: int
|
||||||
|
The numeric identifier of the auction.
|
||||||
|
request_session: Optional[requests.Session], optional
|
||||||
|
A ``requests`` session to reuse connections.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List[Dict[str, Optional[str]]]
|
||||||
|
A list of lot dictionaries with keys ``lot_number``, ``title``,
|
||||||
|
``description``, ``bids``, ``current_bid`` (if available),
|
||||||
|
``image_url`` and ``end_time``. If the endpoint returns no items,
|
||||||
|
an empty list is returned.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
requests.HTTPError
|
||||||
|
If the HTTP call returns a non‑200 response.
|
||||||
|
Exception
|
||||||
|
If the response cannot be decoded as JSON.
|
||||||
|
"""
|
||||||
|
session = request_session or requests.Session()
|
||||||
|
base_url = "https://onlineveilingmeester.nl/rest/nl"
|
||||||
|
url = f"{base_url}/{country}/veilingen/{auction_id}/kavels"
|
||||||
|
response = session.get(url, headers={"Accept": "application/json"}, timeout=30)
|
||||||
|
try:
|
||||||
|
response.raise_for_status()
|
||||||
|
except requests.HTTPError:
|
||||||
|
logger.error("OVM API returned status %s: %s", response.status_code, response.text)
|
||||||
|
raise
|
||||||
|
# Parse the JSON body; expect a list of lots
|
||||||
|
data = response.json()
|
||||||
|
lots: List[Dict[str, Optional[str]]] = []
|
||||||
|
# The response may be a dictionary containing a ``kavels`` key or a list
|
||||||
|
if isinstance(data, dict) and isinstance(data.get("kavels"), list):
|
||||||
|
lot_list = data["kavels"]
|
||||||
|
elif isinstance(data, list):
|
||||||
|
lot_list = data
|
||||||
|
else:
|
||||||
|
logger.error("Unexpected response structure from OVM API: %s", data)
|
||||||
|
raise Exception("Unexpected response structure for OVM lots")
|
||||||
|
for item in lot_list:
|
||||||
|
lot_number = item.get("kavelnummer") or item.get("lotNumber") or item.get("id")
|
||||||
|
title = item.get("naam") or item.get("title")
|
||||||
|
description = item.get("beschrijving") or item.get("description")
|
||||||
|
bids = item.get("aantalBiedingen") or item.get("numberOfBids")
|
||||||
|
# Current bid is nested in ``hoogsteBod`` or ``currentBid``
|
||||||
|
current_bid_obj = item.get("hoogsteBod") or item.get("currentBid")
|
||||||
|
current_bid: Optional[Dict[str, any]] = None
|
||||||
|
if isinstance(current_bid_obj, dict):
|
||||||
|
current_bid = {
|
||||||
|
"amount": current_bid_obj.get("bodBedrag") or current_bid_obj.get("amount"),
|
||||||
|
"currency": current_bid_obj.get("valuta") or current_bid_obj.get("currency"),
|
||||||
|
}
|
||||||
|
image_url = None
|
||||||
|
# OVM may provide a list of image URLs under ``afbeeldingen`` or ``images``
|
||||||
|
if isinstance(item.get("afbeeldingen"), list) and item["afbeeldingen"]:
|
||||||
|
image_url = item["afbeeldingen"][0]
|
||||||
|
elif isinstance(item.get("images"), list) and item["images"]:
|
||||||
|
image_url = item["images"][0].get("url") if isinstance(item["images"][0], dict) else item["images"][0]
|
||||||
|
end_time = item.get("eindDatumISO") or item.get("endDateISO") or item.get("eindDatum")
|
||||||
|
lots.append(
|
||||||
|
{
|
||||||
|
"lot_number": lot_number,
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"bids": bids,
|
||||||
|
"current_bid": current_bid,
|
||||||
|
"image_url": image_url,
|
||||||
|
"end_time": end_time,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return lots
|
||||||
481
utils/auction_items.py
Normal file
481
utils/auction_items.py
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
"""
|
||||||
|
Utility functions to fetch auction items (lots) from different providers.
|
||||||
|
|
||||||
|
This module defines three functions that make HTTP calls to the public APIs of
|
||||||
|
Troostwijk Auctions (TWK), AuctionPort (AP) and Online Veilingmeester (OVM)
|
||||||
|
and normalises their responses into Python dictionaries. Each function
|
||||||
|
returns a list of dictionaries where each dictionary represents an
|
||||||
|
individual lot and includes standardised keys: ``title``, ``description``,
|
||||||
|
``bids`` (the number of bids if available), ``current_bid`` (current price
|
||||||
|
and currency if available), ``image_url`` and ``end_time``.
|
||||||
|
|
||||||
|
The implementations rely on the ``requests`` library for HTTP transport
|
||||||
|
and include basic error handling. They raise ``requests.HTTPError``
|
||||||
|
when the remote server responds with a non‑200 status code.
|
||||||
|
|
||||||
|
Note: the APIs these functions call are subject to change. Endpoints and
|
||||||
|
field names may differ depending on the auction status or provider version.
|
||||||
|
These functions are intended as a starting point for integrating with
|
||||||
|
multiple auction platforms; you may need to adjust query parameters,
|
||||||
|
header values or JSON field names if the provider updates their API.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
```
|
||||||
|
from auction_items import get_items_twk, get_items_ap, get_items_ovm
|
||||||
|
|
||||||
|
# Troostwijk Auctions (TWK): pass the visible auction identifier
|
||||||
|
lots = get_items_twk(display_id="35563")
|
||||||
|
for lot in lots:
|
||||||
|
print(lot['lot_number'], lot['title'], lot['current_bid'])
|
||||||
|
|
||||||
|
# AuctionPort (AP): pass the auction ID from the AuctionPort website
|
||||||
|
ap_lots = get_items_ap(auction_id=1323)
|
||||||
|
|
||||||
|
# Online Veilingmeester (OVM): the country code is required to build the
|
||||||
|
# endpoint path (e.g. ``'nederland'`` or ``'belgie'``) along with the
|
||||||
|
# numeric auction ID.
|
||||||
|
ovm_lots = get_items_ovm(country="nederland", auction_id=7713)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def get_items_twk(
|
||||||
|
display_id: str,
|
||||||
|
*,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 200,
|
||||||
|
locale: str = "nl",
|
||||||
|
sort_by: str = "LOT_NUMBER_ASC",
|
||||||
|
platform: str = "TWK",
|
||||||
|
request_session: Optional[requests.Session] = None,
|
||||||
|
) -> List[Dict[str, Optional[str]]]:
|
||||||
|
"""Fetch lots (items) for a Troostwijk auction using the current GraphQL API.
|
||||||
|
|
||||||
|
Troostwijk’s public GraphQL API exposes a ``lotsByAuctionDisplayId`` query
|
||||||
|
that returns the lots belonging to a specific auction. The input
|
||||||
|
``LotsByAuctionDisplayIdInput`` requires an ``auctionDisplayId``,
|
||||||
|
``locale``, ``pageNumber``, ``pageSize`` and a non‑null ``sortBy``
|
||||||
|
parameter【566587544277629†screenshot】. The platform is provided as the
|
||||||
|
``Platform`` enum value and must be set to ``"TWK"`` for Troostwijk
|
||||||
|
auctions【622367855745945†screenshot】.
|
||||||
|
|
||||||
|
Each result in the ``results`` list is a ``ListingLot`` object that
|
||||||
|
includes fields such as ``number`` (the lot/kavel number), ``title``,
|
||||||
|
``description``, ``bidsCount``, ``currentBidAmount`` (Money object with
|
||||||
|
``cents`` and ``currency``), ``endDate`` (a scalar ``TbaDate``) and
|
||||||
|
``image`` with a ``url`` property【819766814773156†screenshot】. This
|
||||||
|
function builds a query that selects these fields and converts them
|
||||||
|
into a normalised Python dictionary per lot.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
display_id: str
|
||||||
|
The auction display identifier (e.g., the number visible in the
|
||||||
|
auction URL).
|
||||||
|
page: int, optional
|
||||||
|
The page number to retrieve (1‑based). Defaults to 1.
|
||||||
|
page_size: int, optional
|
||||||
|
The number of lots per page. Defaults to 200.
|
||||||
|
locale: str, optional
|
||||||
|
Language code for the returned content (default ``"nl"``).
|
||||||
|
sort_by: str, optional
|
||||||
|
Sorting option from the ``LotsSortingOption`` enum (for example
|
||||||
|
``"LOT_NUMBER_ASC"`` or ``"END_DATE_DESC"``)【566587544277629†screenshot】.
|
||||||
|
Defaults to ``"LOT_NUMBER_ASC"``.
|
||||||
|
platform: str, optional
|
||||||
|
Platform code as defined in the ``Platform`` enum. Use ``"TWK"`` for
|
||||||
|
Troostwijk auctions【622367855745945†screenshot】.
|
||||||
|
request_session: Optional[requests.Session], optional
|
||||||
|
Optional session object for connection pooling.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List[Dict[str, Optional[str]]]
|
||||||
|
A list of dictionaries representing lots. Each dictionary
|
||||||
|
contains the keys ``lot_number``, ``title``, ``description``,
|
||||||
|
``bids`` (number of bids), ``current_bid`` (amount in major units
|
||||||
|
and currency), ``image_url`` and ``end_time``.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
requests.HTTPError
|
||||||
|
If the HTTP call does not return a 2xx status code.
|
||||||
|
Exception
|
||||||
|
If GraphQL returns errors or the response structure is unexpected.
|
||||||
|
"""
|
||||||
|
session = request_session or requests.Session()
|
||||||
|
url = "https://storefront.tbauctions.com/storefront/graphql"
|
||||||
|
# GraphQL query for lots by auction display ID. We request the
|
||||||
|
# ListingLots wrapper (hasNext, pageNumber etc.) and select relevant
|
||||||
|
# fields from each ListingLot【819766814773156†screenshot】. The money
|
||||||
|
# amounts are represented in cents; we convert them to major units.
|
||||||
|
graphql_query = """
|
||||||
|
query LotsByAuctionDisplayId($request: LotsByAuctionDisplayIdInput!, $platform: Platform!) {
|
||||||
|
lotsByAuctionDisplayId(request: $request, platform: $platform) {
|
||||||
|
pageNumber
|
||||||
|
pageSize
|
||||||
|
totalSize
|
||||||
|
hasNext
|
||||||
|
results {
|
||||||
|
number
|
||||||
|
title
|
||||||
|
description
|
||||||
|
bidsCount
|
||||||
|
currentBidAmount {
|
||||||
|
cents
|
||||||
|
currency
|
||||||
|
}
|
||||||
|
endDate
|
||||||
|
image {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
variables = {
|
||||||
|
"request": {
|
||||||
|
"auctionDisplayId": str(display_id),
|
||||||
|
"locale": locale,
|
||||||
|
"pageNumber": page,
|
||||||
|
"pageSize": page_size,
|
||||||
|
"sortBy": sort_by,
|
||||||
|
},
|
||||||
|
"platform": platform,
|
||||||
|
}
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
# Use GET request because the GraphQL endpoint does not accept POST
|
||||||
|
# from unauthenticated clients. We encode the query and variables as
|
||||||
|
# query parameters.
|
||||||
|
try:
|
||||||
|
response = session.get(
|
||||||
|
url,
|
||||||
|
params={"query": graphql_query, "variables": json.dumps(variables)},
|
||||||
|
headers=headers,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Error contacting Troostwijk GraphQL endpoint: %s", exc)
|
||||||
|
raise
|
||||||
|
try:
|
||||||
|
response.raise_for_status()
|
||||||
|
except requests.HTTPError:
|
||||||
|
logger.error("Troostwijk API returned status %s: %s", response.status_code, response.text)
|
||||||
|
raise
|
||||||
|
data = response.json()
|
||||||
|
if "errors" in data and data["errors"]:
|
||||||
|
message = data["errors"]
|
||||||
|
logger.error("GraphQL returned errors: %s", message)
|
||||||
|
raise Exception(f"GraphQL returned errors: {message}")
|
||||||
|
# Extract the list of lots
|
||||||
|
try:
|
||||||
|
lot_items = data["data"]["lotsByAuctionDisplayId"]["results"]
|
||||||
|
except (KeyError, TypeError) as e:
|
||||||
|
logger.error("Unexpected response structure from Troostwijk API: %s", data)
|
||||||
|
raise Exception(f"Unexpected response structure: {e}")
|
||||||
|
lots: List[Dict[str, Optional[str]]] = []
|
||||||
|
for item in lot_items:
|
||||||
|
lot_number = item.get("number")
|
||||||
|
title = item.get("title")
|
||||||
|
description = item.get("description")
|
||||||
|
bids = item.get("bidsCount")
|
||||||
|
bid_amount = item.get("currentBidAmount") or {}
|
||||||
|
# Convert cents to major units if available
|
||||||
|
current_bid = None
|
||||||
|
if bid_amount and isinstance(bid_amount, dict):
|
||||||
|
cents = bid_amount.get("cents")
|
||||||
|
currency = bid_amount.get("currency")
|
||||||
|
if cents is not None:
|
||||||
|
current_bid = {
|
||||||
|
"amount": cents / 100.0,
|
||||||
|
"currency": currency,
|
||||||
|
}
|
||||||
|
end_time = item.get("endDate")
|
||||||
|
image_obj = item.get("image") or {}
|
||||||
|
image_url = image_obj.get("url") if isinstance(image_obj, dict) else None
|
||||||
|
lots.append(
|
||||||
|
{
|
||||||
|
"lot_number": lot_number,
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"bids": bids,
|
||||||
|
"current_bid": current_bid,
|
||||||
|
"image_url": image_url,
|
||||||
|
"end_time": end_time,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return lots
|
||||||
|
|
||||||
|
|
||||||
|
def get_items_ap(
|
||||||
|
auction_id: int,
|
||||||
|
*,
|
||||||
|
request_session: Optional[requests.Session] = None,
|
||||||
|
) -> List[Dict[str, Optional[str]]]:
|
||||||
|
"""Retrieve items (lots) from an AuctionPort auction.
|
||||||
|
|
||||||
|
AuctionPort operates a JSON API on ``https://api.auctionport.be``. While
|
||||||
|
official documentation for the lot endpoints is scarce, community code
|
||||||
|
suggests that auctions can be fetched via ``/auctions/small``【461010206788258†L10-L39】. The
|
||||||
|
corresponding lot information appears to reside under an
|
||||||
|
``/auctions/{id}/lots`` or ``/lots?auctionId={id}`` endpoint (the
|
||||||
|
platform uses XML internally for some pages as observed when visiting
|
||||||
|
``/auctions/{id}/lots`` in a browser). This function attempts to call
|
||||||
|
these endpoints in order and parse their JSON responses. If the
|
||||||
|
response is not JSON, it falls back to a simple text scrape looking
|
||||||
|
for lot numbers, titles, descriptions and current bid amounts.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
auction_id: int
|
||||||
|
The numeric identifier of the auction on AuctionPort.
|
||||||
|
request_session: Optional[requests.Session], optional
|
||||||
|
An existing requests session.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List[Dict[str, Optional[str]]]
|
||||||
|
A list of lot dictionaries with the keys ``lot_number``, ``title``,
|
||||||
|
``description``, ``bids`` (if available), ``current_bid`` (amount and
|
||||||
|
currency if provided), ``image_url`` and ``end_time``. If no lots
|
||||||
|
could be parsed, an empty list is returned.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
requests.HTTPError
|
||||||
|
If both endpoint attempts return non‑200 responses.
|
||||||
|
"""
|
||||||
|
session = request_session or requests.Session()
|
||||||
|
# Candidate endpoints for AuctionPort lots. The first URL follows the
|
||||||
|
# pattern used by the AuctionPort website; the second is a query by
|
||||||
|
# parameter. Additional endpoints can be added if discovered.
|
||||||
|
url_candidates = [
|
||||||
|
f"https://api.auctionport.be/auctions/{auction_id}/lots",
|
||||||
|
f"https://api.auctionport.be/lots?auctionId={auction_id}",
|
||||||
|
]
|
||||||
|
last_error: Optional[Exception] = None
|
||||||
|
for url in url_candidates:
|
||||||
|
try:
|
||||||
|
response = session.get(url, headers={"Accept": "application/json"}, timeout=30)
|
||||||
|
except Exception as exc:
|
||||||
|
# Capture connection errors and continue with the next endpoint
|
||||||
|
last_error = exc
|
||||||
|
continue
|
||||||
|
if response.status_code == 404:
|
||||||
|
# Try the next candidate
|
||||||
|
continue
|
||||||
|
if response.status_code >= 400:
|
||||||
|
last_error = requests.HTTPError(
|
||||||
|
f"AuctionPort API error {response.status_code} for {url}",
|
||||||
|
response=response,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
# If the response is OK, attempt to parse JSON
|
||||||
|
try:
|
||||||
|
data = response.json()
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# Not JSON: fallback to naive parsing of plain text/XML. AuctionPort
|
||||||
|
# sometimes returns XML for lots pages. We'll attempt to extract
|
||||||
|
# structured information using simple patterns.
|
||||||
|
text = response.text
|
||||||
|
lots: List[Dict[str, Optional[str]]] = []
|
||||||
|
# Split by <div class="lot"> like markers (not guaranteed). In the
|
||||||
|
# absence of a stable API specification, heuristics must be used.
|
||||||
|
# Here we use a very simple split on "Lot " followed by a number.
|
||||||
|
import re
|
||||||
|
|
||||||
|
pattern = re.compile(r"\bLot\s+(\d+)\b", re.IGNORECASE)
|
||||||
|
for match in pattern.finditer(text):
|
||||||
|
lot_number = match.group(1)
|
||||||
|
# Attempt to extract the title and description following the
|
||||||
|
# lot number. This heuristic looks for a line break or
|
||||||
|
# sentence after the lot label; adjust as necessary.
|
||||||
|
start = match.end()
|
||||||
|
segment = text[start:start + 300] # arbitrary slice length
|
||||||
|
# Title is the first sentence or line
|
||||||
|
title_match = re.search(r"[:\-]\s*(.*?)\.(?=\s|<)", segment)
|
||||||
|
title = title_match.group(1).strip() if title_match else segment.strip()
|
||||||
|
lots.append({
|
||||||
|
"lot_number": lot_number,
|
||||||
|
"title": title,
|
||||||
|
"description": None,
|
||||||
|
"bids": None,
|
||||||
|
"current_bid": None,
|
||||||
|
"image_url": None,
|
||||||
|
"end_time": None,
|
||||||
|
})
|
||||||
|
if lots:
|
||||||
|
return lots
|
||||||
|
else:
|
||||||
|
# If no lots were extracted, continue to the next candidate
|
||||||
|
last_error = Exception("Unable to parse AuctionPort lots from non‑JSON response")
|
||||||
|
continue
|
||||||
|
# If JSON parsing succeeded, inspect the structure. Some endpoints
|
||||||
|
# return a top‑level object with a ``data`` field containing a list.
|
||||||
|
lots: List[Dict[str, Optional[str]]] = []
|
||||||
|
# Attempt to locate the list of lots: it might be in ``data``, ``lots`` or
|
||||||
|
# another property.
|
||||||
|
candidate_keys = ["lots", "data", "items"]
|
||||||
|
lot_list: Optional[List[Dict[str, any]]] = None
|
||||||
|
for key in candidate_keys:
|
||||||
|
if isinstance(data, dict) and isinstance(data.get(key), list):
|
||||||
|
lot_list = data[key]
|
||||||
|
break
|
||||||
|
# If the response is a list itself (not a dict), treat it as the lot list
|
||||||
|
if lot_list is None and isinstance(data, list):
|
||||||
|
lot_list = data
|
||||||
|
if lot_list is None:
|
||||||
|
# Unknown structure; return empty list
|
||||||
|
return []
|
||||||
|
for item in lot_list:
|
||||||
|
# Map fields according to common names; adjust if your endpoint
|
||||||
|
# uses different property names. Use dict.get to avoid KeyError.
|
||||||
|
lot_number = item.get("lotNumber") or item.get("lotnumber") or item.get("id")
|
||||||
|
title = item.get("title") or item.get("naam")
|
||||||
|
description = item.get("description") or item.get("beschrijving")
|
||||||
|
bids = item.get("numberOfBids") or item.get("bidCount")
|
||||||
|
# Determine current bid: AuctionPort might provide ``currentBid`` or
|
||||||
|
# ``currentPrice`` as an object or numeric value.
|
||||||
|
current_bid_obj = item.get("currentBid") or item.get("currentPrice")
|
||||||
|
current_bid: Optional[Dict[str, any]] = None
|
||||||
|
if isinstance(current_bid_obj, dict):
|
||||||
|
current_bid = {
|
||||||
|
"amount": current_bid_obj.get("amount"),
|
||||||
|
"currency": current_bid_obj.get("currency"),
|
||||||
|
}
|
||||||
|
elif current_bid_obj is not None:
|
||||||
|
current_bid = {"amount": current_bid_obj, "currency": None}
|
||||||
|
# Image
|
||||||
|
image_url = None
|
||||||
|
if isinstance(item.get("images"), list) and item["images"]:
|
||||||
|
image_url = item["images"][0].get("url")
|
||||||
|
elif isinstance(item.get("image"), str):
|
||||||
|
image_url = item.get("image")
|
||||||
|
# End time
|
||||||
|
end_time = item.get("endDateISO") or item.get("closingDateISO") or item.get("closingDate")
|
||||||
|
lots.append(
|
||||||
|
{
|
||||||
|
"lot_number": lot_number,
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"bids": bids,
|
||||||
|
"current_bid": current_bid,
|
||||||
|
"image_url": image_url,
|
||||||
|
"end_time": end_time,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return lots
|
||||||
|
|
||||||
|
# All candidates failed
|
||||||
|
if last_error:
|
||||||
|
raise last_error
|
||||||
|
raise requests.HTTPError(f"Could not fetch lots for AuctionPort auction {auction_id}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_items_ovm(
|
||||||
|
country: str,
|
||||||
|
auction_id: int,
|
||||||
|
*,
|
||||||
|
request_session: Optional[requests.Session] = None,
|
||||||
|
) -> List[Dict[str, Optional[str]]]:
|
||||||
|
"""Fetch lots from an Online Veilingmeester auction.
|
||||||
|
|
||||||
|
Online Veilingmeester's REST API exposes auctions and their lots via
|
||||||
|
endpoints under ``https://onlineveilingmeester.nl/rest/nl``. The
|
||||||
|
AuctionViewer project's source code constructs lot URLs as
|
||||||
|
``{land}/veilingen/{id}/kavels``, where ``land`` is the lower‑cased
|
||||||
|
country name (e.g. ``nederland`` or ``belgie``)【366543684390870†L13-L50】.
|
||||||
|
Therefore, the full path for retrieving the lots of a specific auction
|
||||||
|
is ``https://onlineveilingmeester.nl/rest/nl/{country}/veilingen/{auction_id}/kavels``.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
country: str
|
||||||
|
Lower‑case country name used in the API path (for example
|
||||||
|
``"nederland"`` or ``"belgie"``). The value should correspond to
|
||||||
|
the ``land`` property returned by the OVM auctions endpoint【366543684390870†L13-L50】.
|
||||||
|
auction_id: int
|
||||||
|
The numeric identifier of the auction.
|
||||||
|
request_session: Optional[requests.Session], optional
|
||||||
|
A ``requests`` session to reuse connections.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List[Dict[str, Optional[str]]]
|
||||||
|
A list of lot dictionaries with keys ``lot_number``, ``title``,
|
||||||
|
``description``, ``bids``, ``current_bid`` (if available),
|
||||||
|
``image_url`` and ``end_time``. If the endpoint returns no items,
|
||||||
|
an empty list is returned.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
requests.HTTPError
|
||||||
|
If the HTTP call returns a non‑200 response.
|
||||||
|
Exception
|
||||||
|
If the response cannot be decoded as JSON.
|
||||||
|
"""
|
||||||
|
session = request_session or requests.Session()
|
||||||
|
base_url = "https://onlineveilingmeester.nl/rest/nl"
|
||||||
|
url = f"{base_url}/{country}/veilingen/{auction_id}/kavels"
|
||||||
|
response = session.get(url, headers={"Accept": "application/json"}, timeout=30)
|
||||||
|
try:
|
||||||
|
response.raise_for_status()
|
||||||
|
except requests.HTTPError:
|
||||||
|
logger.error("OVM API returned status %s: %s", response.status_code, response.text)
|
||||||
|
raise
|
||||||
|
# Parse the JSON body; expect a list of lots
|
||||||
|
data = response.json()
|
||||||
|
lots: List[Dict[str, Optional[str]]] = []
|
||||||
|
# The response may be a dictionary containing a ``kavels`` key or a list
|
||||||
|
if isinstance(data, dict) and isinstance(data.get("kavels"), list):
|
||||||
|
lot_list = data["kavels"]
|
||||||
|
elif isinstance(data, list):
|
||||||
|
lot_list = data
|
||||||
|
else:
|
||||||
|
logger.error("Unexpected response structure from OVM API: %s", data)
|
||||||
|
raise Exception("Unexpected response structure for OVM lots")
|
||||||
|
for item in lot_list:
|
||||||
|
lot_number = item.get("kavelnummer") or item.get("lotNumber") or item.get("id")
|
||||||
|
title = item.get("naam") or item.get("title")
|
||||||
|
description = item.get("beschrijving") or item.get("description")
|
||||||
|
bids = item.get("aantalBiedingen") or item.get("numberOfBids")
|
||||||
|
# Current bid is nested in ``hoogsteBod`` or ``currentBid``
|
||||||
|
current_bid_obj = item.get("hoogsteBod") or item.get("currentBid")
|
||||||
|
current_bid: Optional[Dict[str, any]] = None
|
||||||
|
if isinstance(current_bid_obj, dict):
|
||||||
|
current_bid = {
|
||||||
|
"amount": current_bid_obj.get("bodBedrag") or current_bid_obj.get("amount"),
|
||||||
|
"currency": current_bid_obj.get("valuta") or current_bid_obj.get("currency"),
|
||||||
|
}
|
||||||
|
image_url = None
|
||||||
|
# OVM may provide a list of image URLs under ``afbeeldingen`` or ``images``
|
||||||
|
if isinstance(item.get("afbeeldingen"), list) and item["afbeeldingen"]:
|
||||||
|
image_url = item["afbeeldingen"][0]
|
||||||
|
elif isinstance(item.get("images"), list) and item["images"]:
|
||||||
|
image_url = item["images"][0].get("url") if isinstance(item["images"][0], dict) else item["images"][0]
|
||||||
|
end_time = item.get("eindDatumISO") or item.get("endDateISO") or item.get("eindDatum")
|
||||||
|
lots.append(
|
||||||
|
{
|
||||||
|
"lot_number": lot_number,
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"bids": bids,
|
||||||
|
"current_bid": current_bid,
|
||||||
|
"image_url": image_url,
|
||||||
|
"end_time": end_time,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return lots
|
||||||
@@ -2,27 +2,30 @@ import requests
|
|||||||
from traceback import print_exc
|
from traceback import print_exc
|
||||||
from cache import Cache, FileCache
|
from cache import Cache, FileCache
|
||||||
from models.location import Auction, Auctionbrand, Countrycode, Maplocation, JsonEncoder
|
from models.location import Auction, Auctionbrand, Countrycode, Maplocation, JsonEncoder
|
||||||
|
from utils.APutils import getAPAuctions
|
||||||
|
from utils.OVMutils import getOVMAuctions
|
||||||
|
from utils.TWKutils import getTwkAuctions
|
||||||
from utils.locationutils import getGeoLocationByCity
|
from utils.locationutils import getGeoLocationByCity
|
||||||
from utils.helperutils import log
|
from utils.helperutils import log
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import re
|
|
||||||
import math
|
|
||||||
|
|
||||||
def getAuctionlocations(countrycode: Countrycode, clearcache:bool = False):
|
|
||||||
|
def getAuctionlocations(countrycode: Countrycode, clearcache: bool = False):
|
||||||
cachename = 'allauctions_' + countrycode
|
cachename = 'allauctions_' + countrycode
|
||||||
|
|
||||||
log("should clear chache with cachename: " + str(clearcache) + ", " + cachename)
|
log("should clear chache with cachename: " + str(clearcache) + ", " + cachename)
|
||||||
|
|
||||||
if(clearcache):
|
if (clearcache):
|
||||||
res = FileCache.get(cachename, 1)
|
res = FileCache.get(cachename, 1)
|
||||||
else:
|
else:
|
||||||
res = FileCache.get(cachename)
|
res = FileCache.get(cachename)
|
||||||
|
|
||||||
if(res):
|
if (res):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
twkauctions = []
|
twkauctions = []
|
||||||
ovmauctions = []
|
ovmauctions = []
|
||||||
|
apauctions = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
twkauctions = getTwkAuctions(countrycode)
|
twkauctions = getTwkAuctions(countrycode)
|
||||||
@@ -31,159 +34,55 @@ def getAuctionlocations(countrycode: Countrycode, clearcache:bool = False):
|
|||||||
print_exc(e)
|
print_exc(e)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ovmauctions = getOVMAuctions()
|
ovmauctions = getOVMAuctions()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log('something went wrong while running the OVM auctions request')
|
log('something went wrong while running the OVM auctions request')
|
||||||
print_exc(e)
|
print_exc(e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
apauctions = getAPAuctions()
|
||||||
|
except Exception as e:
|
||||||
|
log('something went wrong while running the OVM auctions request')
|
||||||
|
print_exc(e)
|
||||||
|
|
||||||
auctions = [*twkauctions, *ovmauctions]
|
auctions = [*twkauctions, *ovmauctions, *apauctions]
|
||||||
|
# filters all auctions for this geonameid
|
||||||
|
auctions = list(filter(lambda a: a.numberoflots > 0, auctions))
|
||||||
|
|
||||||
for auction in auctions:
|
for auction in auctions:
|
||||||
auction.geonamelocation = getGeoLocationByCity(auction.city, countrycode)
|
auction.geonamelocation = getGeoLocationByCity(auction.city, countrycode)
|
||||||
|
|
||||||
|
# filters all auctions for this geonameid
|
||||||
|
auctions = list(filter(lambda a: a.numberoflots > 0, auctions))
|
||||||
|
|
||||||
geonameids = map(get_geonameid, auctions)
|
geonameids = map(get_geonameid, auctions)
|
||||||
uniquegeonameids = (list(set(geonameids)))
|
uniquegeonameids = (list(set(geonameids)))
|
||||||
|
|
||||||
maplocations = []
|
maplocations = []
|
||||||
|
|
||||||
#loops through the uniques geonameids
|
# loops through the uniques geonameids
|
||||||
for geoid in uniquegeonameids:
|
for geoid in uniquegeonameids:
|
||||||
#filters all auctions for this geonameid
|
|
||||||
geoauctions = list(filter(lambda a: get_geonameid(a) == geoid , auctions))
|
# filters all auctions for this geonameid
|
||||||
if(geoauctions):
|
geoauctions = list(filter(lambda a: get_geonameid(a) == geoid, auctions))
|
||||||
#gets the location (if it has any) for the geolocation
|
if (geoauctions):
|
||||||
|
geoauctions = list({object_.url: object_ for object_ in geoauctions}.values())
|
||||||
|
# gets the location (if it has any) for the geolocation
|
||||||
location = geoauctions[0].geonamelocation
|
location = geoauctions[0].geonamelocation
|
||||||
if(location):
|
if (location):
|
||||||
maplocation = Maplocation(location.latitude, location.longitude, len(geoauctions), location, geoauctions)
|
maplocation = Maplocation(location.latitude, location.longitude, len(geoauctions), location, geoauctions)
|
||||||
maplocations.append(maplocation)
|
maplocations.append(maplocation)
|
||||||
|
|
||||||
for location in maplocations:
|
for location in maplocations:
|
||||||
del location.geonamelocation #removes object which is not used anymore
|
del location.geonamelocation # removes object which is not used anymore
|
||||||
for auction in location.auctions:
|
for auction in location.auctions:
|
||||||
del auction.geonamelocation #removes object to not have duplicate data send to the server
|
del auction.geonamelocation # removes object to not have duplicate data send to the server
|
||||||
|
|
||||||
FileCache.add(cachename, maplocations)
|
FileCache.add(cachename, maplocations)
|
||||||
return maplocations
|
return maplocations
|
||||||
|
|
||||||
|
|
||||||
def get_geonameid(auction):
|
def get_geonameid(auction):
|
||||||
if(auction.geonamelocation):
|
if (auction.geonamelocation):
|
||||||
return auction.geonamelocation.geonameid
|
return auction.geonamelocation.geonameid
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# global twkDataUrl; # = ''; # 'https://www.troostwijkauctions.com/_next/data/' #e6-N0pLHv12LVGS0oYzx6/nl/'
|
|
||||||
|
|
||||||
|
|
||||||
def getTWKUrl():
|
|
||||||
response = requests.get('https://www.troostwijkauctions.com/')
|
|
||||||
if(response.status_code ==200):
|
|
||||||
buildid = re.search(r'"buildId":"([^"]*)', response.text, re.MULTILINE )
|
|
||||||
twkDataUrl = 'https://www.troostwijkauctions.com/_next/data/' + str(buildid[1]) + '/nl/'
|
|
||||||
log('buildid: ' + str(buildid[1]))
|
|
||||||
log('twkDataUrl: ' + twkDataUrl)
|
|
||||||
return twkDataUrl
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def getTwkAuctions(countrycode):
|
|
||||||
cachename = 'TwkAuctions_'+ countrycode
|
|
||||||
res = Cache.get(cachename)
|
|
||||||
if(res):
|
|
||||||
return res
|
|
||||||
|
|
||||||
# buildidresponse = requests.get('https://www.troostwijkauctions.com/')
|
|
||||||
twkDataUrl = getTWKUrl()
|
|
||||||
|
|
||||||
if(twkDataUrl is None):
|
|
||||||
return []
|
|
||||||
|
|
||||||
response = requests.get(twkDataUrl + "auctions.json?countries=" + countrycode)
|
|
||||||
|
|
||||||
if(response.status_code ==200):
|
|
||||||
log('Got Twk Auctions')
|
|
||||||
data = response.json()
|
|
||||||
auctions = []
|
|
||||||
|
|
||||||
totalAuctionCount = data['pageProps']['totalSize'];
|
|
||||||
pages = math.ceil(totalAuctionCount / data['pageProps']['pageSize'])
|
|
||||||
# for result in data['pageProps']['auctionList']:
|
|
||||||
|
|
||||||
for i in range(1,pages,1):
|
|
||||||
log("getting page " + str(i) + ' of ' + str(pages))
|
|
||||||
if(i > 1):
|
|
||||||
response = requests.get(twkDataUrl + "auctions.json?countries=" + countrycode + "&page=" + str(i));
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
for twka in data['pageProps']['listData']:
|
|
||||||
# print(twka['urlSlug'])
|
|
||||||
auction = getTWKAuction(twkDataUrl, twka['urlSlug'])
|
|
||||||
if(auction):
|
|
||||||
auctions.append(auction)
|
|
||||||
Cache.add(cachename, auctions)
|
|
||||||
|
|
||||||
return auctions
|
|
||||||
return []
|
|
||||||
|
|
||||||
def getTWKAuction(twkDataUrl, auctionurlslug):
|
|
||||||
log("getting TWK auctiondetails:" + twkDataUrl + "a/" + auctionurlslug + ".json")
|
|
||||||
response = requests.get(twkDataUrl + "a/" + auctionurlslug + '.json')
|
|
||||||
if(response.status_code == 200):
|
|
||||||
data = response.json()
|
|
||||||
if(len(data['pageProps']['lots']['results']) ==0):
|
|
||||||
return None
|
|
||||||
|
|
||||||
twka = data['pageProps']['auction']
|
|
||||||
firstlot = data['pageProps']['lots']['results'][0]
|
|
||||||
city = "Nederland" if firstlot['location']['city'].lower() == 'online' or firstlot['location']['city'].lower() == "free delivery" else firstlot['location']['city']
|
|
||||||
a = Auction(Auctionbrand.TWK, city, firstlot['location']['countryCode'].upper(), twka['name'], datetime.fromtimestamp(twka['startDate']), datetime.fromtimestamp(twka['minEndDate']), '/a/' + auctionurlslug, twka['image']['url'], twka['lotCount'] )
|
|
||||||
# print(a);
|
|
||||||
return a
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def getOVMAuctions():
|
|
||||||
cachename = 'OnlineVeiling_'
|
|
||||||
res = Cache.get(cachename)
|
|
||||||
if(res):
|
|
||||||
return res
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get("https://onlineveilingmeester.nl/rest/nl/veilingen?status=open&domein=ONLINEVEILINGMEESTER")
|
|
||||||
except:
|
|
||||||
log("The OVM auctions call threw a error")
|
|
||||||
|
|
||||||
if(response is None):
|
|
||||||
return []
|
|
||||||
|
|
||||||
if(response.status_code ==200):
|
|
||||||
log('Got Ovm Auctions')
|
|
||||||
try:
|
|
||||||
data = response.json()
|
|
||||||
auctions = []
|
|
||||||
for result in data['veilingen']:
|
|
||||||
cityname ="Nederland" if result['isBezorgVeiling'] else result['afgifteAdres']['plaats']
|
|
||||||
cityname = "Nederland" if cityname is None else cityname #there can be auctions where you have to make an appointment to retrieve the lots
|
|
||||||
startdatetime = result['openingsDatumISO'].replace("T", " ").replace("Z", "")
|
|
||||||
enddatetime = result['sluitingsDatumISO'].replace("T", " ").replace("Z", "")
|
|
||||||
image = ""
|
|
||||||
#if hasattr(result, 'image') : #result['image'] :
|
|
||||||
image = result.get('image', "") #['image']
|
|
||||||
if image == "":
|
|
||||||
images = result.get('imageList')
|
|
||||||
if(len(images) >0):
|
|
||||||
image = images[0]
|
|
||||||
else:
|
|
||||||
log("No image found for OVM auction: " + result['naam'])
|
|
||||||
|
|
||||||
a = Auction(Auctionbrand.OVM, cityname,result['land'], result['naam'],startdatetime, enddatetime, str(result['land']).lower() + '/veilingen/' + str(result['id']) + '/kavels', 'images/150x150/' + image, result['totaalKavels'] )
|
|
||||||
auctions.append(a)
|
|
||||||
Cache.add(cachename, auctions)
|
|
||||||
return auctions
|
|
||||||
except Exception as e:
|
|
||||||
log(e.__cause__ + '-- Something went wrong in the mapping of OVM auctions to auctionviewer objects. The reason was: ' + response.reason + '. The response was: ' + JsonEncoder().encode(response.json()))
|
|
||||||
print_exc(e)
|
|
||||||
|
|
||||||
else:
|
|
||||||
log("The OVM auctions call didn't gave a 200 response but a " + str(response.status_code) + ". With the reason: " + response.reason)
|
|
||||||
return []
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
def log(value):
|
def log(value):
|
||||||
value = value.encode('ascii', errors='ignore')
|
value = value.encode('ascii', errors='ignore')
|
||||||
value = value.decode()
|
value = value.decode()
|
||||||
|
|||||||
6
utils/lots.py
Normal file
6
utils/lots.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from auction_items import get_items_twk
|
||||||
|
|
||||||
|
# Replace "35563" with a real auction ID from troostwijkauctions.com
|
||||||
|
lots = get_items_twk(display_id="35563")
|
||||||
|
for lot in lots:
|
||||||
|
print(lot['lot_number'], lot['title'], lot['current_bid'])
|
||||||
160
web/auctionviewer (1).html
Normal file
160
web/auctionviewer (1).html
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Auctionviewer – Map</title>
|
||||||
|
<!-- Leaflet CSS from alternative CDN -->
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
html, body { height: 100%; margin: 0; padding: 0; }
|
||||||
|
#map { height: 100%; width: 100%; }
|
||||||
|
.auction-marker {
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
#loading {
|
||||||
|
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||||||
|
z-index: 1000; background: white; padding: 20px; border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
#error {
|
||||||
|
position: absolute; top: 10px; left: 10px; right: 10px;
|
||||||
|
z-index: 1000; background: #fee; color: #c00; padding: 15px;
|
||||||
|
border-radius: 4px; display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="loading">Loading map library...</div>
|
||||||
|
<div id="error"></div>
|
||||||
|
<div id="map"></div>
|
||||||
|
|
||||||
|
<!-- Leaflet JS from alternative CDN -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// DEBUG: Check if Leaflet loaded immediately after script tag
|
||||||
|
console.log("Leaflet immediate check:", typeof L !== 'undefined');
|
||||||
|
|
||||||
|
const locations = [
|
||||||
|
{
|
||||||
|
lat: 52.36861,
|
||||||
|
long: 5.2375,
|
||||||
|
numberofauctions: 2,
|
||||||
|
auctions: [
|
||||||
|
{
|
||||||
|
city: "Almere",
|
||||||
|
countrycode: "NL",
|
||||||
|
name: "Verzamelveiling van verschillende goederen",
|
||||||
|
starttime: "2025-11-14 14:00:00",
|
||||||
|
closingtime: "2025-11-27 14:00:00",
|
||||||
|
url: "/a/verzamelveiling-van-verschillende-goederen-A1-34291",
|
||||||
|
imageurl: "https://media.tbauctions.com/image-media/d4d302ab-513d-4f6a-8cdf-e8259612dc9e/file",
|
||||||
|
numberoflots: 198,
|
||||||
|
brand: "TWK",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
city: "Almere",
|
||||||
|
countrycode: "NL",
|
||||||
|
name: "Veiling bedrijfsovertrekking",
|
||||||
|
starttime: "2025-11-21 14:00:00",
|
||||||
|
closingtime: "2025-12-04 14:00:00",
|
||||||
|
url: "/a/veiling-bedrijfsovertrekking-A1-34292",
|
||||||
|
imageurl: "https://media.tbauctions.com/image-media/c5bbc716-3de8-4860-a29d-6e1587f7d42e/file",
|
||||||
|
numberoflots: 247,
|
||||||
|
brand: "TWK",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lat: 51.92442,
|
||||||
|
long: 4.47773,
|
||||||
|
numberofauctions: 1,
|
||||||
|
auctions: [
|
||||||
|
{
|
||||||
|
city: "Rotterdam",
|
||||||
|
countrycode: "NL",
|
||||||
|
name: "Rotterdam Havenveiling",
|
||||||
|
starttime: "2025-11-20 10:00:00",
|
||||||
|
closingtime: "2025-12-03 18:00:00",
|
||||||
|
url: "/a/rotterdam-havenveiling-A1-34293",
|
||||||
|
imageurl: "https://media.tbauctions.com/image-media/placeholder/placeholder/file",
|
||||||
|
numberoflots: 156,
|
||||||
|
brand: "RHA",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Use window.load to ensure ALL resources are loaded
|
||||||
|
window.addEventListener("load", function () {
|
||||||
|
console.log("Window loaded, L is:", typeof L);
|
||||||
|
|
||||||
|
// CRITICAL: Check if Leaflet is actually available
|
||||||
|
if (typeof L === 'undefined') {
|
||||||
|
const errorMsg = "ERROR: Leaflet library failed to load. Check:<br>1. Internet connection<br>2. Browser console (F12)<br>3. Try clearing browser cache<br>4. Ensure not blocking CDN with ad-blocker";
|
||||||
|
document.getElementById('error').innerHTML = errorMsg;
|
||||||
|
document.getElementById('error').style.display = 'block';
|
||||||
|
document.getElementById('loading').style.display = 'none';
|
||||||
|
console.error("Leaflet not loaded. Check network tab in developer tools.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide loading indicator
|
||||||
|
document.getElementById('loading').style.display = 'none';
|
||||||
|
|
||||||
|
// Initialize map
|
||||||
|
const map = L.map("map").setView([52.1326, 5.2913], 8);
|
||||||
|
|
||||||
|
// Add tile layer
|
||||||
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||||
|
maxZoom: 18,
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Add markers
|
||||||
|
locations.forEach((location) => {
|
||||||
|
const { lat, long, numberofauctions, auctions } = location;
|
||||||
|
|
||||||
|
const icon = L.divIcon({
|
||||||
|
className: "auction-marker",
|
||||||
|
html: numberofauctions.toString(),
|
||||||
|
iconSize: [30, 30],
|
||||||
|
iconAnchor: [15, 15],
|
||||||
|
});
|
||||||
|
|
||||||
|
const marker = L.marker([lat, long], { icon });
|
||||||
|
|
||||||
|
let popupHtml = `<strong>${numberofauctions} veiling(en) in ${auctions[0].city}</strong>`;
|
||||||
|
popupHtml += "<ul style='padding-left: 1em;'>";
|
||||||
|
auctions.forEach((a) => {
|
||||||
|
popupHtml += `<li style='margin-bottom: 0.5em;'>`;
|
||||||
|
popupHtml += `<span style='font-weight: bold;'>${a.name}</span><br />`;
|
||||||
|
popupHtml += `Start: ${a.starttime}<br />`;
|
||||||
|
popupHtml += `Sluiting: ${a.closingtime}<br />`;
|
||||||
|
popupHtml += `Lots: ${a.numberoflots}<br />`;
|
||||||
|
popupHtml += `<a href="${a.url}" target="_blank" rel="noopener">Bekijk veiling</a>`;
|
||||||
|
popupHtml += `</li>`;
|
||||||
|
});
|
||||||
|
popupHtml += "</ul>";
|
||||||
|
|
||||||
|
marker.bindPopup(popupHtml);
|
||||||
|
marker.addTo(map);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Map initialized successfully with", locations.length, "locations");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
661
web/leaflet.css
Normal file
661
web/leaflet.css
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
/* required styles */
|
||||||
|
|
||||||
|
.leaflet-pane,
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-tile-container,
|
||||||
|
.leaflet-pane > svg,
|
||||||
|
.leaflet-pane > canvas,
|
||||||
|
.leaflet-zoom-box,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-layer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
/* Prevents IE11 from highlighting tiles in blue */
|
||||||
|
.leaflet-tile::selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||||
|
.leaflet-safari .leaflet-tile {
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
}
|
||||||
|
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||||
|
.leaflet-safari .leaflet-tile-container {
|
||||||
|
width: 1600px;
|
||||||
|
height: 1600px;
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||||
|
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||||
|
.leaflet-container .leaflet-overlay-pane svg {
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
.leaflet-container .leaflet-marker-pane img,
|
||||||
|
.leaflet-container .leaflet-shadow-pane img,
|
||||||
|
.leaflet-container .leaflet-tile-pane img,
|
||||||
|
.leaflet-container img.leaflet-image-layer,
|
||||||
|
.leaflet-container .leaflet-tile {
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
width: auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container img.leaflet-tile {
|
||||||
|
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
||||||
|
mix-blend-mode: plus-lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: pan-x pan-y;
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag {
|
||||||
|
-ms-touch-action: pinch-zoom;
|
||||||
|
/* Fallback for FF which doesn't support pinch-zoom */
|
||||||
|
touch-action: none;
|
||||||
|
touch-action: pinch-zoom;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tile {
|
||||||
|
filter: inherit;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile-loaded {
|
||||||
|
visibility: inherit;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 800;
|
||||||
|
}
|
||||||
|
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||||
|
.leaflet-overlay-pane svg {
|
||||||
|
-moz-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-pane { z-index: 400; }
|
||||||
|
|
||||||
|
.leaflet-tile-pane { z-index: 200; }
|
||||||
|
.leaflet-overlay-pane { z-index: 400; }
|
||||||
|
.leaflet-shadow-pane { z-index: 500; }
|
||||||
|
.leaflet-marker-pane { z-index: 600; }
|
||||||
|
.leaflet-tooltip-pane { z-index: 650; }
|
||||||
|
.leaflet-popup-pane { z-index: 700; }
|
||||||
|
|
||||||
|
.leaflet-map-pane canvas { z-index: 100; }
|
||||||
|
.leaflet-map-pane svg { z-index: 200; }
|
||||||
|
|
||||||
|
.leaflet-vml-shape {
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
.lvml {
|
||||||
|
behavior: url(#default#VML);
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* control positioning */
|
||||||
|
|
||||||
|
.leaflet-control {
|
||||||
|
position: relative;
|
||||||
|
z-index: 800;
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-top,
|
||||||
|
.leaflet-bottom {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.leaflet-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.leaflet-left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control {
|
||||||
|
float: left;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.leaflet-top .leaflet-control {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* zoom and fade animations */
|
||||||
|
|
||||||
|
.leaflet-fade-anim .leaflet-popup {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: opacity 0.2s linear;
|
||||||
|
-moz-transition: opacity 0.2s linear;
|
||||||
|
transition: opacity 0.2s linear;
|
||||||
|
}
|
||||||
|
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-animated {
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
-ms-transform-origin: 0 0;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
svg.leaflet-zoom-animated {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||||
|
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
}
|
||||||
|
.leaflet-zoom-anim .leaflet-tile,
|
||||||
|
.leaflet-pan-anim .leaflet-tile {
|
||||||
|
-webkit-transition: none;
|
||||||
|
-moz-transition: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* cursors */
|
||||||
|
|
||||||
|
.leaflet-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.leaflet-grab {
|
||||||
|
cursor: -webkit-grab;
|
||||||
|
cursor: -moz-grab;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
.leaflet-crosshair,
|
||||||
|
.leaflet-crosshair .leaflet-interactive {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
.leaflet-popup-pane,
|
||||||
|
.leaflet-control {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
.leaflet-dragging .leaflet-grab,
|
||||||
|
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||||
|
.leaflet-dragging .leaflet-marker-draggable {
|
||||||
|
cursor: move;
|
||||||
|
cursor: -webkit-grabbing;
|
||||||
|
cursor: -moz-grabbing;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* marker & overlays interactivity */
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-pane > svg path,
|
||||||
|
.leaflet-tile-container {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-marker-icon.leaflet-interactive,
|
||||||
|
.leaflet-image-layer.leaflet-interactive,
|
||||||
|
.leaflet-pane > svg path.leaflet-interactive,
|
||||||
|
svg.leaflet-image-layer.leaflet-interactive path {
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* visual tweaks */
|
||||||
|
|
||||||
|
.leaflet-container {
|
||||||
|
background: #ddd;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
color: #0078A8;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
border: 2px dotted #38f;
|
||||||
|
background: rgba(255,255,255,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general typography */
|
||||||
|
.leaflet-container {
|
||||||
|
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general toolbar styles */
|
||||||
|
|
||||||
|
.leaflet-bar {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
line-height: 26px;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.leaflet-bar a,
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-position: 50% 50%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:hover,
|
||||||
|
.leaflet-bar a:focus {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.leaflet-bar a.leaflet-disabled {
|
||||||
|
cursor: default;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-bar a {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 2px;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* zoom control */
|
||||||
|
|
||||||
|
.leaflet-control-zoom-in,
|
||||||
|
.leaflet-control-zoom-out {
|
||||||
|
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||||
|
text-indent: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* layers control */
|
||||||
|
|
||||||
|
.leaflet-control-layers {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers.png);
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
.leaflet-retina .leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers-2x.png);
|
||||||
|
background-size: 26px 26px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers-toggle {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers .leaflet-control-layers-list,
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded {
|
||||||
|
padding: 6px 10px 6px 6px;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-scrollbar {
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-selector {
|
||||||
|
margin-top: 2px;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-size: 1.08333em;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-separator {
|
||||||
|
height: 0;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
margin: 5px -10px 5px -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default icon URLs */
|
||||||
|
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||||
|
background-image: url(images/marker-icon.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* attribution and scale controls */
|
||||||
|
|
||||||
|
.leaflet-container .leaflet-control-attribution {
|
||||||
|
background: #fff;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution,
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
padding: 0 5px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a:hover,
|
||||||
|
.leaflet-control-attribution a:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.leaflet-attribution-flag {
|
||||||
|
display: inline !important;
|
||||||
|
vertical-align: baseline !important;
|
||||||
|
width: 1em;
|
||||||
|
height: 0.6669em;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control-scale {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control-scale {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
border: 2px solid #777;
|
||||||
|
border-top: none;
|
||||||
|
line-height: 1.1;
|
||||||
|
padding: 2px 5px 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
text-shadow: 1px 1px #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child) {
|
||||||
|
border-top: 2px solid #777;
|
||||||
|
border-bottom: none;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||||
|
border-bottom: 2px solid #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-attribution,
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
border: 2px solid rgba(0,0,0,0.2);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* popup */
|
||||||
|
|
||||||
|
.leaflet-popup {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper {
|
||||||
|
padding: 1px;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content {
|
||||||
|
margin: 13px 24px 13px 20px;
|
||||||
|
line-height: 1.3;
|
||||||
|
font-size: 13px;
|
||||||
|
font-size: 1.08333em;
|
||||||
|
min-height: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content p {
|
||||||
|
margin: 17px 0;
|
||||||
|
margin: 1.3em 0;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip-container {
|
||||||
|
width: 40px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -1px;
|
||||||
|
margin-left: -20px;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
padding: 1px;
|
||||||
|
|
||||||
|
margin: -10px auto 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
-moz-transform: rotate(45deg);
|
||||||
|
-ms-transform: rotate(45deg);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
background: white;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
font: 16px/24px Tahoma, Verdana, sans-serif;
|
||||||
|
color: #757575;
|
||||||
|
text-decoration: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:hover,
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:focus {
|
||||||
|
color: #585858;
|
||||||
|
}
|
||||||
|
.leaflet-popup-scrolled {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||||
|
-ms-zoom: 1;
|
||||||
|
}
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
width: 24px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||||
|
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-control-zoom,
|
||||||
|
.leaflet-oldie .leaflet-control-layers,
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
border: 1px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* div icon */
|
||||||
|
|
||||||
|
.leaflet-div-icon {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Tooltip */
|
||||||
|
/* Base styles for the element that has a tooltip */
|
||||||
|
.leaflet-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #222;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tooltip.leaflet-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before,
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
border: 6px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Directions */
|
||||||
|
|
||||||
|
.leaflet-tooltip-bottom {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top {
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
bottom: 0;
|
||||||
|
margin-bottom: -12px;
|
||||||
|
border-top-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before {
|
||||||
|
top: 0;
|
||||||
|
margin-top: -12px;
|
||||||
|
margin-left: -6px;
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left {
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before {
|
||||||
|
right: 0;
|
||||||
|
margin-right: -12px;
|
||||||
|
border-left-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
left: 0;
|
||||||
|
margin-left: -12px;
|
||||||
|
border-right-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Printing */
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
/* Prevent printers from removing background-images of controls. */
|
||||||
|
.leaflet-control {
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
web/leaflet.js
Normal file
5
web/leaflet.js
Normal file
File diff suppressed because one or more lines are too long
10
web/test.html
Normal file
10
web/test.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Title</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user