add: type annotation to waybackpy modules
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@@ -10,37 +11,41 @@ from .exceptions import (
|
|||||||
)
|
)
|
||||||
from .utils import DEFAULT_USER_AGENT
|
from .utils import DEFAULT_USER_AGENT
|
||||||
|
|
||||||
|
ResponseJSON = Dict[str, Any]
|
||||||
|
|
||||||
class WaybackMachineAvailabilityAPI:
|
|
||||||
|
class WaybackMachineAvailabilityAPI(object):
|
||||||
"""
|
"""
|
||||||
Class that interfaces the availability API of the Wayback Machine.
|
Class that interfaces the availability API of the Wayback Machine.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, url, user_agent=DEFAULT_USER_AGENT, max_tries=3):
|
def __init__(
|
||||||
|
self, url: str, user_agent: str = DEFAULT_USER_AGENT, max_tries: int = 3
|
||||||
|
) -> None:
|
||||||
self.url = str(url).strip().replace(" ", "%20")
|
self.url = str(url).strip().replace(" ", "%20")
|
||||||
self.user_agent = user_agent
|
self.user_agent = user_agent
|
||||||
self.headers = {"User-Agent": self.user_agent}
|
self.headers: Dict[str, str] = {"User-Agent": self.user_agent}
|
||||||
self.payload = {"url": "{url}".format(url=self.url)}
|
self.payload = {"url": "{url}".format(url=self.url)}
|
||||||
self.endpoint = "https://archive.org/wayback/available"
|
self.endpoint = "https://archive.org/wayback/available"
|
||||||
self.max_tries = max_tries
|
self.max_tries = max_tries
|
||||||
self.tries = 0
|
self.tries = 0
|
||||||
self.last_api_call_unix_time = int(time.time())
|
self.last_api_call_unix_time = int(time.time())
|
||||||
self.api_call_time_gap = 5
|
self.api_call_time_gap = 5
|
||||||
self.JSON = None
|
self.JSON: Optional[ResponseJSON] = None
|
||||||
|
|
||||||
def unix_timestamp_to_wayback_timestamp(self, unix_timestamp):
|
def unix_timestamp_to_wayback_timestamp(self, unix_timestamp: int) -> str:
|
||||||
"""
|
"""
|
||||||
Converts Unix time to wayback Machine timestamp.
|
Converts Unix time to wayback Machine timestamp.
|
||||||
"""
|
"""
|
||||||
return datetime.utcfromtimestamp(int(unix_timestamp)).strftime("%Y%m%d%H%M%S")
|
return datetime.utcfromtimestamp(int(unix_timestamp)).strftime("%Y%m%d%H%M%S")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
"""
|
"""
|
||||||
Same as string representation, just return the archive URL as a string.
|
Same as string representation, just return the archive URL as a string.
|
||||||
"""
|
"""
|
||||||
return str(self)
|
return str(self)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
"""
|
"""
|
||||||
String representation of the class. If atleast one API call was successfully
|
String representation of the class. If atleast one API call was successfully
|
||||||
made then return the archive URL as a string. Else returns None.
|
made then return the archive URL as a string. Else returns None.
|
||||||
@@ -54,7 +59,7 @@ class WaybackMachineAvailabilityAPI:
|
|||||||
|
|
||||||
return self.archive_url
|
return self.archive_url
|
||||||
|
|
||||||
def json(self):
|
def json(self) -> Optional[ResponseJSON]:
|
||||||
"""
|
"""
|
||||||
Makes the API call to the availability API can set the JSON response
|
Makes the API call to the availability API can set the JSON response
|
||||||
to the JSON attribute of the instance and also returns the JSON attribute.
|
to the JSON attribute of the instance and also returns the JSON attribute.
|
||||||
@@ -79,7 +84,7 @@ class WaybackMachineAvailabilityAPI:
|
|||||||
|
|
||||||
return self.JSON
|
return self.JSON
|
||||||
|
|
||||||
def timestamp(self):
|
def timestamp(self) -> datetime:
|
||||||
"""
|
"""
|
||||||
Converts the timestamp form the JSON response to datetime object.
|
Converts the timestamp form the JSON response to datetime object.
|
||||||
If JSON attribute of the instance is None it implies that the either
|
If JSON attribute of the instance is None it implies that the either
|
||||||
@@ -91,19 +96,29 @@ class WaybackMachineAvailabilityAPI:
|
|||||||
If you get an URL as a response form the availability API it is guaranteed
|
If you get an URL as a response form the availability API it is guaranteed
|
||||||
that you can get the datetime object from the timestamp.
|
that you can get the datetime object from the timestamp.
|
||||||
"""
|
"""
|
||||||
if not self.JSON or not self.JSON["archived_snapshots"]:
|
if self.JSON is None or "archived_snapshots" not in self.JSON:
|
||||||
return datetime.max
|
return datetime.max
|
||||||
|
elif (
|
||||||
return datetime.strptime(
|
self.JSON is not None
|
||||||
self.JSON["archived_snapshots"]["closest"]["timestamp"], "%Y%m%d%H%M%S"
|
and "archived_snapshots" in self.JSON
|
||||||
)
|
and self.JSON["archived_snapshots"] is not None
|
||||||
|
and "closest" in self.JSON["archived_snapshots"]
|
||||||
|
and self.JSON["archived_snapshots"]["closest"] is not None
|
||||||
|
and "timestamp" in self.JSON["archived_snapshots"]["closest"]
|
||||||
|
):
|
||||||
|
return datetime.strptime(
|
||||||
|
self.JSON["archived_snapshots"]["closest"]["timestamp"], "%Y%m%d%H%M%S"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError("Could not get timestamp from result")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def archive_url(self):
|
def archive_url(self) -> str:
|
||||||
"""
|
"""
|
||||||
Reads the the JSON response data and tries to get the timestamp and returns
|
Reads the the JSON response data and tries to get the timestamp and returns
|
||||||
the timestamp if found else returns None.
|
the timestamp if found else returns None.
|
||||||
"""
|
"""
|
||||||
|
archive_url = ""
|
||||||
data = self.JSON
|
data = self.JSON
|
||||||
|
|
||||||
# If the user didn't used oldest, newest or near but tries to access the
|
# If the user didn't used oldest, newest or near but tries to access the
|
||||||
@@ -138,7 +153,7 @@ class WaybackMachineAvailabilityAPI:
|
|||||||
)
|
)
|
||||||
return archive_url
|
return archive_url
|
||||||
|
|
||||||
def wayback_timestamp(self, **kwargs):
|
def wayback_timestamp(self, **kwargs: int) -> str:
|
||||||
"""
|
"""
|
||||||
Prepends zero before the year, month, day, hour and minute so that they
|
Prepends zero before the year, month, day, hour and minute so that they
|
||||||
are conformable with the YYYYMMDDhhmmss wayback machine timestamp format.
|
are conformable with the YYYYMMDDhhmmss wayback machine timestamp format.
|
||||||
@@ -148,7 +163,7 @@ class WaybackMachineAvailabilityAPI:
|
|||||||
for key in ["year", "month", "day", "hour", "minute"]
|
for key in ["year", "month", "day", "hour", "minute"]
|
||||||
)
|
)
|
||||||
|
|
||||||
def oldest(self):
|
def oldest(self) -> "WaybackMachineAvailabilityAPI":
|
||||||
"""
|
"""
|
||||||
Passing the year 1994 should return the oldest archive because
|
Passing the year 1994 should return the oldest archive because
|
||||||
wayback machine was started in May, 1996 and there should be no archive
|
wayback machine was started in May, 1996 and there should be no archive
|
||||||
@@ -156,7 +171,7 @@ class WaybackMachineAvailabilityAPI:
|
|||||||
"""
|
"""
|
||||||
return self.near(year=1994)
|
return self.near(year=1994)
|
||||||
|
|
||||||
def newest(self):
|
def newest(self) -> "WaybackMachineAvailabilityAPI":
|
||||||
"""
|
"""
|
||||||
Passing the current UNIX time should be sufficient to get the newest
|
Passing the current UNIX time should be sufficient to get the newest
|
||||||
archive considering the API request-response time delay and also the
|
archive considering the API request-response time delay and also the
|
||||||
@@ -166,13 +181,13 @@ class WaybackMachineAvailabilityAPI:
|
|||||||
|
|
||||||
def near(
|
def near(
|
||||||
self,
|
self,
|
||||||
year=None,
|
year: Optional[int] = None,
|
||||||
month=None,
|
month: Optional[int] = None,
|
||||||
day=None,
|
day: Optional[int] = None,
|
||||||
hour=None,
|
hour: Optional[int] = None,
|
||||||
minute=None,
|
minute: Optional[int] = None,
|
||||||
unix_timestamp=None,
|
unix_timestamp: Optional[int] = None,
|
||||||
):
|
) -> "WaybackMachineAvailabilityAPI":
|
||||||
"""
|
"""
|
||||||
The main method for this Class, oldest and newest methods are dependent on this
|
The main method for this Class, oldest and newest methods are dependent on this
|
||||||
method.
|
method.
|
||||||
@@ -188,11 +203,11 @@ class WaybackMachineAvailabilityAPI:
|
|||||||
else:
|
else:
|
||||||
now = datetime.utcnow().timetuple()
|
now = datetime.utcnow().timetuple()
|
||||||
timestamp = self.wayback_timestamp(
|
timestamp = self.wayback_timestamp(
|
||||||
year=year if year else now.tm_year,
|
year=now.tm_year if year is None else year,
|
||||||
month=month if month else now.tm_mon,
|
month=now.tm_mon if month is None else month,
|
||||||
day=day if day else now.tm_mday,
|
day=now.tm_mday if day is None else day,
|
||||||
hour=hour if hour else now.tm_hour,
|
hour=now.tm_hour if hour is None else hour,
|
||||||
minute=minute if minute else now.tm_min,
|
minute=now.tm_min if minute is None else minute,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.payload["timestamp"] = timestamp
|
self.payload["timestamp"] = timestamp
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Dict, Generator, List, Optional, cast
|
||||||
|
|
||||||
from .cdx_snapshot import CDXSnapshot
|
from .cdx_snapshot import CDXSnapshot
|
||||||
from .cdx_utils import (
|
from .cdx_utils import (
|
||||||
check_collapses,
|
check_collapses,
|
||||||
@@ -11,43 +13,48 @@ from .exceptions import WaybackError
|
|||||||
from .utils import DEFAULT_USER_AGENT
|
from .utils import DEFAULT_USER_AGENT
|
||||||
|
|
||||||
|
|
||||||
class WaybackMachineCDXServerAPI:
|
class WaybackMachineCDXServerAPI(object):
|
||||||
"""
|
"""
|
||||||
Class that interfaces the CDX server API of the Wayback Machine.
|
Class that interfaces the CDX server API of the Wayback Machine.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# start_timestamp: from, can not use from as it's a keyword
|
||||||
|
# end_timestamp: to, not using to as can not use from
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
url,
|
url: str,
|
||||||
user_agent=DEFAULT_USER_AGENT,
|
user_agent: str = DEFAULT_USER_AGENT,
|
||||||
start_timestamp=None, # from, can not use from as it's a keyword
|
start_timestamp: Optional[str] = None,
|
||||||
end_timestamp=None, # to, not using to as can not use from
|
end_timestamp: Optional[str] = None,
|
||||||
filters=[],
|
filters: List[str] = [],
|
||||||
match_type=None,
|
match_type: Optional[str] = None,
|
||||||
gzip=None,
|
gzip: Optional[str] = None,
|
||||||
collapses=[],
|
collapses: List[str] = [],
|
||||||
limit=None,
|
limit: Optional[str] = None,
|
||||||
max_tries=3,
|
max_tries: int = 3,
|
||||||
):
|
) -> None:
|
||||||
self.url = str(url).strip().replace(" ", "%20")
|
self.url = str(url).strip().replace(" ", "%20")
|
||||||
self.user_agent = user_agent
|
self.user_agent = user_agent
|
||||||
self.start_timestamp = str(start_timestamp) if start_timestamp else None
|
self.start_timestamp = (
|
||||||
self.end_timestamp = str(end_timestamp) if end_timestamp else None
|
str(start_timestamp) if start_timestamp is not None else None
|
||||||
|
)
|
||||||
|
self.end_timestamp = str(end_timestamp) if end_timestamp is not None else None
|
||||||
self.filters = filters
|
self.filters = filters
|
||||||
check_filters(self.filters)
|
check_filters(self.filters)
|
||||||
self.match_type = str(match_type).strip() if match_type else None
|
self.match_type = str(match_type).strip() if match_type is not None else None
|
||||||
check_match_type(self.match_type, self.url)
|
check_match_type(self.match_type, self.url)
|
||||||
self.gzip = gzip if gzip else True
|
self.gzip = gzip
|
||||||
self.collapses = collapses
|
self.collapses = collapses
|
||||||
check_collapses(self.collapses)
|
check_collapses(self.collapses)
|
||||||
self.limit = limit if limit else 5000
|
self.limit = limit if limit is not None else 5000
|
||||||
self.max_tries = max_tries
|
self.max_tries = max_tries
|
||||||
self.last_api_request_url = None
|
self.last_api_request_url: Optional[str] = None
|
||||||
self.use_page = False
|
self.use_page = False
|
||||||
self.endpoint = "https://web.archive.org/cdx/search/cdx"
|
self.endpoint = "https://web.archive.org/cdx/search/cdx"
|
||||||
|
|
||||||
def cdx_api_manager(self, payload, headers, use_page=False):
|
def cdx_api_manager(
|
||||||
|
self, payload: Dict[str, str], headers: Dict[str, str], use_page: bool = False
|
||||||
|
) -> Generator[str, None, None]:
|
||||||
total_pages = get_total_pages(self.url, self.user_agent)
|
total_pages = get_total_pages(self.url, self.user_agent)
|
||||||
# If we only have two or less pages of archives then we care for more accuracy
|
# If we only have two or less pages of archives then we care for more accuracy
|
||||||
# pagination API is lagged sometimes
|
# pagination API is lagged sometimes
|
||||||
@@ -58,6 +65,8 @@ class WaybackMachineCDXServerAPI:
|
|||||||
|
|
||||||
url = full_url(self.endpoint, params=payload)
|
url = full_url(self.endpoint, params=payload)
|
||||||
res = get_response(url, headers=headers)
|
res = get_response(url, headers=headers)
|
||||||
|
if isinstance(res, Exception):
|
||||||
|
raise res
|
||||||
|
|
||||||
self.last_api_request_url = url
|
self.last_api_request_url = url
|
||||||
text = res.text
|
text = res.text
|
||||||
@@ -69,19 +78,18 @@ class WaybackMachineCDXServerAPI:
|
|||||||
|
|
||||||
yield text
|
yield text
|
||||||
else:
|
else:
|
||||||
|
|
||||||
payload["showResumeKey"] = "true"
|
payload["showResumeKey"] = "true"
|
||||||
payload["limit"] = str(self.limit)
|
payload["limit"] = str(self.limit)
|
||||||
resumeKey = None
|
resumeKey = None
|
||||||
|
|
||||||
more = True
|
more = True
|
||||||
while more:
|
while more:
|
||||||
|
|
||||||
if resumeKey:
|
if resumeKey:
|
||||||
payload["resumeKey"] = resumeKey
|
payload["resumeKey"] = resumeKey
|
||||||
|
|
||||||
url = full_url(self.endpoint, params=payload)
|
url = full_url(self.endpoint, params=payload)
|
||||||
res = get_response(url, headers=headers)
|
res = get_response(url, headers=headers)
|
||||||
|
if isinstance(res, Exception):
|
||||||
|
raise res
|
||||||
|
|
||||||
self.last_api_request_url = url
|
self.last_api_request_url = url
|
||||||
|
|
||||||
@@ -102,14 +110,14 @@ class WaybackMachineCDXServerAPI:
|
|||||||
|
|
||||||
yield text
|
yield text
|
||||||
|
|
||||||
def add_payload(self, payload):
|
def add_payload(self, payload: Dict[str, str]) -> None:
|
||||||
if self.start_timestamp:
|
if self.start_timestamp:
|
||||||
payload["from"] = self.start_timestamp
|
payload["from"] = self.start_timestamp
|
||||||
|
|
||||||
if self.end_timestamp:
|
if self.end_timestamp:
|
||||||
payload["to"] = self.end_timestamp
|
payload["to"] = self.end_timestamp
|
||||||
|
|
||||||
if self.gzip is not True:
|
if self.gzip is None:
|
||||||
payload["gzip"] = "false"
|
payload["gzip"] = "false"
|
||||||
|
|
||||||
if self.match_type:
|
if self.match_type:
|
||||||
@@ -126,8 +134,8 @@ class WaybackMachineCDXServerAPI:
|
|||||||
# Don't need to return anything as it's dictionary.
|
# Don't need to return anything as it's dictionary.
|
||||||
payload["url"] = self.url
|
payload["url"] = self.url
|
||||||
|
|
||||||
def snapshots(self):
|
def snapshots(self) -> Generator[CDXSnapshot, None, None]:
|
||||||
payload = {}
|
payload: Dict[str, str] = {}
|
||||||
headers = {"User-Agent": self.user_agent}
|
headers = {"User-Agent": self.user_agent}
|
||||||
|
|
||||||
self.add_payload(payload)
|
self.add_payload(payload)
|
||||||
@@ -152,7 +160,7 @@ class WaybackMachineCDXServerAPI:
|
|||||||
if len(snapshot) < 46: # 14 + 32 (timestamp+digest)
|
if len(snapshot) < 46: # 14 + 32 (timestamp+digest)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
properties = {
|
properties: Dict[str, Optional[str]] = {
|
||||||
"urlkey": None,
|
"urlkey": None,
|
||||||
"timestamp": None,
|
"timestamp": None,
|
||||||
"original": None,
|
"original": None,
|
||||||
@@ -190,4 +198,4 @@ class WaybackMachineCDXServerAPI:
|
|||||||
properties["length"],
|
properties["length"],
|
||||||
) = prop_values
|
) = prop_values
|
||||||
|
|
||||||
yield CDXSnapshot(properties)
|
yield CDXSnapshot(cast(Dict[str, str], properties))
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
class CDXSnapshot:
|
class CDXSnapshot(object):
|
||||||
"""
|
"""
|
||||||
Class for the CDX snapshot lines returned by the CDX API,
|
Class for the CDX snapshot lines returned by the CDX API,
|
||||||
Each valid line of the CDX API is casted to an CDXSnapshot object
|
Each valid line of the CDX API is casted to an CDXSnapshot object
|
||||||
@@ -10,7 +11,7 @@ class CDXSnapshot:
|
|||||||
of the CDXSnapshot.
|
of the CDXSnapshot.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, properties):
|
def __init__(self, properties: Dict[str, str]) -> None:
|
||||||
self.urlkey = properties["urlkey"]
|
self.urlkey = properties["urlkey"]
|
||||||
self.timestamp = properties["timestamp"]
|
self.timestamp = properties["timestamp"]
|
||||||
self.datetime_timestamp = datetime.strptime(self.timestamp, "%Y%m%d%H%M%S")
|
self.datetime_timestamp = datetime.strptime(self.timestamp, "%Y%m%d%H%M%S")
|
||||||
@@ -23,7 +24,7 @@ class CDXSnapshot:
|
|||||||
"https://web.archive.org/web/" + self.timestamp + "/" + self.original
|
"https://web.archive.org/web/" + self.timestamp + "/" + self.original
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return "{urlkey} {timestamp} {original} {mimetype} {statuscode} {digest} {length}".format(
|
return "{urlkey} {timestamp} {original} {mimetype} {statuscode} {digest} {length}".format(
|
||||||
urlkey=self.urlkey,
|
urlkey=self.urlkey,
|
||||||
timestamp=self.timestamp,
|
timestamp=self.timestamp,
|
||||||
|
@@ -1,23 +1,30 @@
|
|||||||
import re
|
import re
|
||||||
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from requests.adapters import HTTPAdapter
|
from requests.adapters import HTTPAdapter
|
||||||
from urllib3.util.retry import Retry
|
|
||||||
|
# from urllib3.util.retry import Retry
|
||||||
|
from requests.packages.urllib3.util.retry import Retry
|
||||||
|
|
||||||
from .exceptions import WaybackError
|
from .exceptions import WaybackError
|
||||||
from .utils import DEFAULT_USER_AGENT
|
from .utils import DEFAULT_USER_AGENT
|
||||||
|
|
||||||
|
|
||||||
def get_total_pages(url, user_agent=DEFAULT_USER_AGENT):
|
def get_total_pages(url: str, user_agent: str = DEFAULT_USER_AGENT) -> int:
|
||||||
endpoint = "https://web.archive.org/cdx/search/cdx?"
|
endpoint = "https://web.archive.org/cdx/search/cdx?"
|
||||||
payload = {"showNumPages": "true", "url": str(url)}
|
payload = {"showNumPages": "true", "url": str(url)}
|
||||||
headers = {"User-Agent": user_agent}
|
headers = {"User-Agent": user_agent}
|
||||||
request_url = full_url(endpoint, params=payload)
|
request_url = full_url(endpoint, params=payload)
|
||||||
response = get_response(request_url, headers=headers)
|
response = get_response(request_url, headers=headers)
|
||||||
return int(response.text.strip())
|
if isinstance(response, requests.Response):
|
||||||
|
return int(response.text.strip())
|
||||||
|
else:
|
||||||
|
raise response
|
||||||
|
|
||||||
|
|
||||||
def full_url(endpoint, params):
|
def full_url(endpoint: str, params: Dict[str, Any]) -> str:
|
||||||
if not params:
|
if not params:
|
||||||
return endpoint
|
return endpoint
|
||||||
full_url = endpoint if endpoint.endswith("?") else (endpoint + "?")
|
full_url = endpoint if endpoint.endswith("?") else (endpoint + "?")
|
||||||
@@ -26,27 +33,25 @@ def full_url(endpoint, params):
|
|||||||
key = "collapse" if key.startswith("collapse") else key
|
key = "collapse" if key.startswith("collapse") else key
|
||||||
amp = "" if full_url.endswith("?") else "&"
|
amp = "" if full_url.endswith("?") else "&"
|
||||||
full_url = (
|
full_url = (
|
||||||
full_url
|
full_url + amp + "{key}={val}".format(key=key, val=quote(str(val), safe=""))
|
||||||
+ amp
|
|
||||||
+ "{key}={val}".format(key=key, val=requests.utils.quote(str(val)))
|
|
||||||
)
|
)
|
||||||
return full_url
|
return full_url
|
||||||
|
|
||||||
|
|
||||||
def get_response(
|
def get_response(
|
||||||
url,
|
url: str,
|
||||||
headers=None,
|
headers: Optional[Dict[str, str]] = None,
|
||||||
retries=5,
|
retries: int = 5,
|
||||||
backoff_factor=0.5,
|
backoff_factor: float = 0.5,
|
||||||
no_raise_on_redirects=False,
|
# no_raise_on_redirects=False,
|
||||||
):
|
) -> Union[requests.Response, Exception]:
|
||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
retries = Retry(
|
retries_ = Retry(
|
||||||
total=retries,
|
total=retries,
|
||||||
backoff_factor=backoff_factor,
|
backoff_factor=backoff_factor,
|
||||||
status_forcelist=[500, 502, 503, 504],
|
status_forcelist=[500, 502, 503, 504],
|
||||||
)
|
)
|
||||||
session.mount("https://", HTTPAdapter(max_retries=retries))
|
session.mount("https://", HTTPAdapter(max_retries=retries_))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = session.get(url, headers=headers)
|
response = session.get(url, headers=headers)
|
||||||
@@ -62,23 +67,18 @@ def get_response(
|
|||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
|
|
||||||
def check_filters(filters):
|
def check_filters(filters: List[str]) -> None:
|
||||||
if not isinstance(filters, list):
|
if not isinstance(filters, list):
|
||||||
raise WaybackError("filters must be a list.")
|
raise WaybackError("filters must be a list.")
|
||||||
|
|
||||||
# [!]field:regex
|
# [!]field:regex
|
||||||
for _filter in filters:
|
for _filter in filters:
|
||||||
try:
|
match = re.search(
|
||||||
|
r"(\!?(?:urlkey|timestamp|original|mimetype|statuscode|digest|length)):(.*)",
|
||||||
|
_filter,
|
||||||
|
)
|
||||||
|
|
||||||
match = re.search(
|
if match is None or len(match.groups()) != 2:
|
||||||
r"(\!?(?:urlkey|timestamp|original|mimetype|statuscode|digest|length)):(.*)",
|
|
||||||
_filter,
|
|
||||||
)
|
|
||||||
|
|
||||||
match.group(1)
|
|
||||||
match.group(2)
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
|
|
||||||
exc_message = (
|
exc_message = (
|
||||||
"Filter '{_filter}' is not following the cdx filter syntax.".format(
|
"Filter '{_filter}' is not following the cdx filter syntax.".format(
|
||||||
@@ -88,43 +88,38 @@ def check_filters(filters):
|
|||||||
raise WaybackError(exc_message)
|
raise WaybackError(exc_message)
|
||||||
|
|
||||||
|
|
||||||
def check_collapses(collapses):
|
def check_collapses(collapses: List[str]) -> bool:
|
||||||
|
|
||||||
if not isinstance(collapses, list):
|
if not isinstance(collapses, list):
|
||||||
raise WaybackError("collapses must be a list.")
|
raise WaybackError("collapses must be a list.")
|
||||||
|
elif len(collapses) == 0:
|
||||||
if len(collapses) == 0:
|
return True
|
||||||
return
|
|
||||||
|
|
||||||
for collapse in collapses:
|
for collapse in collapses:
|
||||||
try:
|
match = re.search(
|
||||||
match = re.search(
|
r"(urlkey|timestamp|original|mimetype|statuscode|digest|length)(:?[0-9]{1,99})?",
|
||||||
r"(urlkey|timestamp|original|mimetype|statuscode|digest|length)(:?[0-9]{1,99})?",
|
collapse,
|
||||||
collapse,
|
)
|
||||||
)
|
if match is None or len(match.groups()) != 2:
|
||||||
match.group(1)
|
|
||||||
if 2 == len(match.groups()):
|
|
||||||
match.group(2)
|
|
||||||
except Exception:
|
|
||||||
exc_message = "collapse argument '{collapse}' is not following the cdx collapse syntax.".format(
|
exc_message = "collapse argument '{collapse}' is not following the cdx collapse syntax.".format(
|
||||||
collapse=collapse
|
collapse=collapse
|
||||||
)
|
)
|
||||||
raise WaybackError(exc_message)
|
raise WaybackError(exc_message)
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def check_match_type(match_type, url):
|
def check_match_type(match_type: Optional[str], url: str) -> bool:
|
||||||
|
legal_match_type = ["exact", "prefix", "host", "domain"]
|
||||||
if not match_type:
|
if not match_type:
|
||||||
return
|
return True
|
||||||
|
elif "*" in url:
|
||||||
if "*" in url:
|
|
||||||
raise WaybackError(
|
raise WaybackError(
|
||||||
"Can not use wildcard in the URL along with the match_type arguments."
|
"Can not use wildcard in the URL along with the match_type arguments."
|
||||||
)
|
)
|
||||||
|
elif match_type not in legal_match_type:
|
||||||
legal_match_type = ["exact", "prefix", "host", "domain"]
|
|
||||||
|
|
||||||
if match_type not in legal_match_type:
|
|
||||||
exc_message = "{match_type} is not an allowed match type.\nUse one from 'exact', 'prefix', 'host' or 'domain'".format(
|
exc_message = "{match_type} is not an allowed match type.\nUse one from 'exact', 'prefix', 'host' or 'domain'".format(
|
||||||
match_type=match_type
|
match_type=match_type
|
||||||
)
|
)
|
||||||
raise WaybackError(exc_message)
|
raise WaybackError(exc_message)
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
@@ -3,6 +3,7 @@ import os
|
|||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
|
from typing import Generator, List, Optional
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import requests
|
import requests
|
||||||
@@ -163,34 +164,34 @@ from .wrapper import Url
|
|||||||
+ "will be printed.",
|
+ "will be printed.",
|
||||||
)
|
)
|
||||||
def main(
|
def main(
|
||||||
url,
|
url: Optional[str],
|
||||||
user_agent,
|
user_agent: str,
|
||||||
version,
|
version: bool,
|
||||||
license,
|
license: bool,
|
||||||
newest,
|
newest: bool,
|
||||||
oldest,
|
oldest: bool,
|
||||||
json,
|
json: bool,
|
||||||
near,
|
near: bool,
|
||||||
year,
|
year: Optional[int],
|
||||||
month,
|
month: Optional[int],
|
||||||
day,
|
day: Optional[int],
|
||||||
hour,
|
hour: Optional[int],
|
||||||
minute,
|
minute: Optional[int],
|
||||||
save,
|
save: bool,
|
||||||
headers,
|
headers: bool,
|
||||||
known_urls,
|
known_urls: bool,
|
||||||
subdomain,
|
subdomain: bool,
|
||||||
file,
|
file: bool,
|
||||||
cdx,
|
cdx: bool,
|
||||||
start_timestamp,
|
start_timestamp: Optional[str],
|
||||||
end_timestamp,
|
end_timestamp: Optional[str],
|
||||||
filter,
|
filter: List[str],
|
||||||
match_type,
|
match_type: Optional[str],
|
||||||
gzip,
|
gzip: Optional[str],
|
||||||
collapse,
|
collapse: List[str],
|
||||||
limit,
|
limit: Optional[str],
|
||||||
cdx_print,
|
cdx_print: List[str],
|
||||||
):
|
) -> None:
|
||||||
"""\b
|
"""\b
|
||||||
_ _
|
_ _
|
||||||
| | | |
|
| | | |
|
||||||
@@ -244,7 +245,9 @@ def main(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
def echo_availability_api(availability_api_instance):
|
def echo_availability_api(
|
||||||
|
availability_api_instance: WaybackMachineAvailabilityAPI,
|
||||||
|
) -> None:
|
||||||
click.echo("Archive URL:")
|
click.echo("Archive URL:")
|
||||||
if not availability_api_instance.archive_url:
|
if not availability_api_instance.archive_url:
|
||||||
archive_url = (
|
archive_url = (
|
||||||
@@ -295,13 +298,14 @@ def main(
|
|||||||
click.echo(save_api.headers)
|
click.echo(save_api.headers)
|
||||||
return
|
return
|
||||||
|
|
||||||
def save_urls_on_file(url_gen):
|
def save_urls_on_file(url_gen: Generator[str, None, None]) -> None:
|
||||||
domain = None
|
domain = None
|
||||||
sys_random = random.SystemRandom()
|
sys_random = random.SystemRandom()
|
||||||
uid = "".join(
|
uid = "".join(
|
||||||
sys_random.choice(string.ascii_lowercase + string.digits) for _ in range(6)
|
sys_random.choice(string.ascii_lowercase + string.digits) for _ in range(6)
|
||||||
)
|
)
|
||||||
url_count = 0
|
url_count = 0
|
||||||
|
file_name = None
|
||||||
|
|
||||||
for url in url_gen:
|
for url in url_gen:
|
||||||
url_count += 1
|
url_count += 1
|
||||||
@@ -310,7 +314,7 @@ def main(
|
|||||||
|
|
||||||
domain = "domain-unknown"
|
domain = "domain-unknown"
|
||||||
|
|
||||||
if match:
|
if match is not None:
|
||||||
domain = match.group(1)
|
domain = match.group(1)
|
||||||
|
|
||||||
file_name = "{domain}-urls-{uid}.txt".format(domain=domain, uid=uid)
|
file_name = "{domain}-urls-{uid}.txt".format(domain=domain, uid=uid)
|
||||||
@@ -318,12 +322,12 @@ def main(
|
|||||||
if not os.path.isfile(file_path):
|
if not os.path.isfile(file_path):
|
||||||
open(file_path, "w+").close()
|
open(file_path, "w+").close()
|
||||||
|
|
||||||
with open(file_path, "a") as f:
|
with open(file_path, "a") as f:
|
||||||
f.write("{url}\n".format(url=url))
|
f.write("{url}\n".format(url=url))
|
||||||
|
|
||||||
click.echo(url)
|
click.echo(url)
|
||||||
|
|
||||||
if url_count > 0:
|
if url_count > 0 or file_name is not None:
|
||||||
click.echo(
|
click.echo(
|
||||||
"\n\n'{file_name}' saved in current working directory".format(
|
"\n\n'{file_name}' saved in current working directory".format(
|
||||||
file_name=file_name
|
file_name=file_name
|
||||||
|
@@ -14,6 +14,8 @@ class WaybackError(Exception):
|
|||||||
All other exceptions are inherited from this class.
|
All other exceptions are inherited from this class.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class RedirectSaveError(WaybackError):
|
class RedirectSaveError(WaybackError):
|
||||||
"""
|
"""
|
||||||
@@ -21,32 +23,44 @@ class RedirectSaveError(WaybackError):
|
|||||||
redirect URL is archived but not the original URL.
|
redirect URL is archived but not the original URL.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class URLError(Exception):
|
class URLError(Exception):
|
||||||
"""
|
"""
|
||||||
Raised when malformed URLs are passed as arguments.
|
Raised when malformed URLs are passed as arguments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MaximumRetriesExceeded(WaybackError):
|
class MaximumRetriesExceeded(WaybackError):
|
||||||
"""
|
"""
|
||||||
MaximumRetriesExceeded
|
MaximumRetriesExceeded
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MaximumSaveRetriesExceeded(MaximumRetriesExceeded):
|
class MaximumSaveRetriesExceeded(MaximumRetriesExceeded):
|
||||||
"""
|
"""
|
||||||
MaximumSaveRetriesExceeded
|
MaximumSaveRetriesExceeded
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ArchiveNotInAvailabilityAPIResponse(WaybackError):
|
class ArchiveNotInAvailabilityAPIResponse(WaybackError):
|
||||||
"""
|
"""
|
||||||
Could not parse the archive in the JSON response of the availability API.
|
Could not parse the archive in the JSON response of the availability API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InvalidJSONInAvailabilityAPIResponse(WaybackError):
|
class InvalidJSONInAvailabilityAPIResponse(WaybackError):
|
||||||
"""
|
"""
|
||||||
availability api returned invalid JSON
|
availability api returned invalid JSON
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
@@ -1,38 +1,42 @@
|
|||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from requests.adapters import HTTPAdapter
|
from requests.adapters import HTTPAdapter
|
||||||
from urllib3.util.retry import Retry
|
|
||||||
|
# from urllib3.util.retry import Retry
|
||||||
|
from requests.packages.urllib3.util.retry import Retry
|
||||||
|
|
||||||
from .exceptions import MaximumSaveRetriesExceeded
|
from .exceptions import MaximumSaveRetriesExceeded
|
||||||
from .utils import DEFAULT_USER_AGENT
|
from .utils import DEFAULT_USER_AGENT
|
||||||
|
|
||||||
|
|
||||||
class WaybackMachineSaveAPI:
|
class WaybackMachineSaveAPI(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
WaybackMachineSaveAPI class provides an interface for saving URLs on the
|
WaybackMachineSaveAPI class provides an interface for saving URLs on the
|
||||||
Wayback Machine.
|
Wayback Machine.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, url, user_agent=DEFAULT_USER_AGENT, max_tries=8):
|
def __init__(
|
||||||
|
self, url: str, user_agent: str = DEFAULT_USER_AGENT, max_tries: int = 8
|
||||||
|
) -> None:
|
||||||
self.url = str(url).strip().replace(" ", "%20")
|
self.url = str(url).strip().replace(" ", "%20")
|
||||||
self.request_url = "https://web.archive.org/save/" + self.url
|
self.request_url = "https://web.archive.org/save/" + self.url
|
||||||
self.user_agent = user_agent
|
self.user_agent = user_agent
|
||||||
self.request_headers = {"User-Agent": self.user_agent}
|
self.request_headers: Dict[str, str] = {"User-Agent": self.user_agent}
|
||||||
if max_tries < 1:
|
if max_tries < 1:
|
||||||
raise ValueError("max_tries should be positive")
|
raise ValueError("max_tries should be positive")
|
||||||
self.max_tries = max_tries
|
self.max_tries = max_tries
|
||||||
self.total_save_retries = 5
|
self.total_save_retries = 5
|
||||||
self.backoff_factor = 0.5
|
self.backoff_factor = 0.5
|
||||||
self.status_forcelist = [500, 502, 503, 504]
|
self.status_forcelist = [500, 502, 503, 504]
|
||||||
self._archive_url = None
|
self._archive_url: Optional[str] = None
|
||||||
self.instance_birth_time = datetime.utcnow()
|
self.instance_birth_time = datetime.utcnow()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def archive_url(self):
|
def archive_url(self) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the archive URL is already cached by _archive_url
|
Returns the archive URL is already cached by _archive_url
|
||||||
else invoke the save method to save the archive which returns the
|
else invoke the save method to save the archive which returns the
|
||||||
@@ -44,7 +48,7 @@ class WaybackMachineSaveAPI:
|
|||||||
else:
|
else:
|
||||||
return self.save()
|
return self.save()
|
||||||
|
|
||||||
def get_save_request_headers(self):
|
def get_save_request_headers(self) -> None:
|
||||||
"""
|
"""
|
||||||
Creates a session and tries 'retries' number of times to
|
Creates a session and tries 'retries' number of times to
|
||||||
retrieve the archive.
|
retrieve the archive.
|
||||||
@@ -61,21 +65,21 @@ class WaybackMachineSaveAPI:
|
|||||||
the response URL yourself in the browser.
|
the response URL yourself in the browser.
|
||||||
"""
|
"""
|
||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
retries = Retry(
|
retries_ = Retry(
|
||||||
total=self.total_save_retries,
|
total=self.total_save_retries,
|
||||||
backoff_factor=self.backoff_factor,
|
backoff_factor=self.backoff_factor,
|
||||||
status_forcelist=self.status_forcelist,
|
status_forcelist=self.status_forcelist,
|
||||||
)
|
)
|
||||||
session.mount("https://", HTTPAdapter(max_retries=retries))
|
session.mount("https://", HTTPAdapter(max_retries=retries_))
|
||||||
self.response = session.get(self.request_url, headers=self.request_headers)
|
self.response = session.get(self.request_url, headers=self.request_headers)
|
||||||
self.headers = (
|
# requests.response.headers is requests.structures.CaseInsensitiveDict
|
||||||
self.response.headers
|
self.headers = self.response.headers
|
||||||
) # <class 'requests.structures.CaseInsensitiveDict'>
|
self.headers_str = str(self.headers)
|
||||||
self.status_code = self.response.status_code
|
self.status_code = self.response.status_code
|
||||||
self.response_url = self.response.url
|
self.response_url = self.response.url
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
def archive_url_parser(self):
|
def archive_url_parser(self) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Three regexen (like oxen?) are used to search for the
|
Three regexen (like oxen?) are used to search for the
|
||||||
archive URL in the headers and finally look in the response URL
|
archive URL in the headers and finally look in the response URL
|
||||||
@@ -83,18 +87,18 @@ class WaybackMachineSaveAPI:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
regex1 = r"Content-Location: (/web/[0-9]{14}/.*)"
|
regex1 = r"Content-Location: (/web/[0-9]{14}/.*)"
|
||||||
match = re.search(regex1, str(self.headers))
|
match = re.search(regex1, self.headers_str)
|
||||||
if match:
|
if match:
|
||||||
return "https://web.archive.org" + match.group(1)
|
return "https://web.archive.org" + match.group(1)
|
||||||
|
|
||||||
regex2 = r"rel=\"memento.*?(web\.archive\.org/web/[0-9]{14}/.*?)>"
|
regex2 = r"rel=\"memento.*?(web\.archive\.org/web/[0-9]{14}/.*?)>"
|
||||||
match = re.search(regex2, str(self.headers))
|
match = re.search(regex2, self.headers_str)
|
||||||
if match:
|
if match is not None and len(match.groups()) == 1:
|
||||||
return "https://" + match.group(1)
|
return "https://" + match.group(1)
|
||||||
|
|
||||||
regex3 = r"X-Cache-Key:\shttps(.*)[A-Z]{2}"
|
regex3 = r"X-Cache-Key:\shttps(.*)[A-Z]{2}"
|
||||||
match = re.search(regex3, str(self.headers))
|
match = re.search(regex3, self.headers_str)
|
||||||
if match:
|
if match is not None and len(match.groups()) == 1:
|
||||||
return "https" + match.group(1)
|
return "https" + match.group(1)
|
||||||
|
|
||||||
if self.response_url:
|
if self.response_url:
|
||||||
@@ -105,7 +109,9 @@ class WaybackMachineSaveAPI:
|
|||||||
if match:
|
if match:
|
||||||
return "https://" + match.group(0)
|
return "https://" + match.group(0)
|
||||||
|
|
||||||
def sleep(self, tries):
|
return None
|
||||||
|
|
||||||
|
def sleep(self, tries: int) -> None:
|
||||||
"""
|
"""
|
||||||
Ensure that the we wait some time before succesive retries so that we
|
Ensure that the we wait some time before succesive retries so that we
|
||||||
don't waste the retries before the page is even captured by the Wayback
|
don't waste the retries before the page is even captured by the Wayback
|
||||||
@@ -120,7 +126,7 @@ class WaybackMachineSaveAPI:
|
|||||||
sleep_seconds = 10
|
sleep_seconds = 10
|
||||||
time.sleep(sleep_seconds)
|
time.sleep(sleep_seconds)
|
||||||
|
|
||||||
def timestamp(self):
|
def timestamp(self) -> datetime:
|
||||||
"""
|
"""
|
||||||
Read the timestamp off the archive URL and convert the Wayback Machine
|
Read the timestamp off the archive URL and convert the Wayback Machine
|
||||||
timestamp to datetime object.
|
timestamp to datetime object.
|
||||||
@@ -133,9 +139,10 @@ class WaybackMachineSaveAPI:
|
|||||||
didn't serve a Cached URL. It is quite common for the Wayback Machine to serve
|
didn't serve a Cached URL. It is quite common for the Wayback Machine to serve
|
||||||
cached archive if last archive was captured before last 45 minutes.
|
cached archive if last archive was captured before last 45 minutes.
|
||||||
"""
|
"""
|
||||||
m = re.search(
|
regex = r"https?://web\.archive.org/web/([0-9]{14})/http"
|
||||||
r"https?://web\.archive.org/web/([0-9]{14})/http", self._archive_url
|
m = re.search(regex, str(self._archive_url))
|
||||||
)
|
if m is None or len(m.groups()) != 1:
|
||||||
|
raise ValueError("Could not find get timestamp")
|
||||||
string_timestamp = m.group(1)
|
string_timestamp = m.group(1)
|
||||||
timestamp = datetime.strptime(string_timestamp, "%Y%m%d%H%M%S")
|
timestamp = datetime.strptime(string_timestamp, "%Y%m%d%H%M%S")
|
||||||
|
|
||||||
@@ -149,7 +156,7 @@ class WaybackMachineSaveAPI:
|
|||||||
|
|
||||||
return timestamp
|
return timestamp
|
||||||
|
|
||||||
def save(self):
|
def save(self) -> str:
|
||||||
"""
|
"""
|
||||||
Calls the SavePageNow API of the Wayback Machine with required parameters
|
Calls the SavePageNow API of the Wayback Machine with required parameters
|
||||||
and headers to save the URL.
|
and headers to save the URL.
|
||||||
@@ -169,7 +176,7 @@ class WaybackMachineSaveAPI:
|
|||||||
self.get_save_request_headers()
|
self.get_save_request_headers()
|
||||||
self.saved_archive = self.archive_url_parser()
|
self.saved_archive = self.archive_url_parser()
|
||||||
|
|
||||||
if self.saved_archive is not None:
|
if isinstance(self.saved_archive, str):
|
||||||
self._archive_url = self.saved_archive
|
self._archive_url = self.saved_archive
|
||||||
self.timestamp()
|
self.timestamp()
|
||||||
return self.saved_archive
|
return self.saved_archive
|
||||||
@@ -179,5 +186,5 @@ class WaybackMachineSaveAPI:
|
|||||||
raise MaximumSaveRetriesExceeded(
|
raise MaximumSaveRetriesExceeded(
|
||||||
"Tried %s times but failed to save and retrieve the" % str(tries)
|
"Tried %s times but failed to save and retrieve the" % str(tries)
|
||||||
+ " archive for %s.\nResponse URL:\n%s \nResponse Header:\n%s\n"
|
+ " archive for %s.\nResponse URL:\n%s \nResponse Header:\n%s\n"
|
||||||
% (self.url, self.response_url, str(self.headers)),
|
% (self.url, self.response_url, self.headers_str),
|
||||||
)
|
)
|
||||||
|
@@ -2,22 +2,43 @@ import requests
|
|||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
|
|
||||||
DEFAULT_USER_AGENT = "waybackpy %s - https://github.com/akamhy/waybackpy" % __version__
|
DEFAULT_USER_AGENT: str = (
|
||||||
|
"waybackpy %s - https://github.com/akamhy/waybackpy" % __version__
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def latest_version_pypi(package_name, user_agent=DEFAULT_USER_AGENT):
|
def latest_version_pypi(package_name: str, user_agent: str = DEFAULT_USER_AGENT) -> str:
|
||||||
request_url = "https://pypi.org/pypi/" + package_name + "/json"
|
request_url = "https://pypi.org/pypi/" + package_name + "/json"
|
||||||
headers = {"User-Agent": user_agent}
|
headers = {"User-Agent": user_agent}
|
||||||
response = requests.get(request_url, headers=headers)
|
response = requests.get(request_url, headers=headers)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
return data["info"]["version"]
|
if (
|
||||||
|
data is not None
|
||||||
|
and "info" in data
|
||||||
|
and data["info"] is not None
|
||||||
|
and "version" in data["info"]
|
||||||
|
and data["info"]["version"] is not None
|
||||||
|
):
|
||||||
|
return str(data["info"]["version"])
|
||||||
|
else:
|
||||||
|
raise ValueError("Could not get latest pypi version")
|
||||||
|
|
||||||
|
|
||||||
def latest_version_github(package_name, user_agent=DEFAULT_USER_AGENT):
|
def latest_version_github(
|
||||||
|
package_name: str, user_agent: str = DEFAULT_USER_AGENT
|
||||||
|
) -> str:
|
||||||
request_url = (
|
request_url = (
|
||||||
"https://api.github.com/repos/akamhy/" + package_name + "/releases?per_page=1"
|
"https://api.github.com/repos/akamhy/" + package_name + "/releases?per_page=1"
|
||||||
)
|
)
|
||||||
headers = {"User-Agent": user_agent}
|
headers = {"User-Agent": user_agent}
|
||||||
response = requests.get(request_url, headers=headers)
|
response = requests.get(request_url, headers=headers)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
return data[0]["tag_name"]
|
if (
|
||||||
|
data is not None
|
||||||
|
and len(data) > 0
|
||||||
|
and data[0] is not None
|
||||||
|
and "tag_name" in data[0]
|
||||||
|
):
|
||||||
|
return str(data[0]["tag_name"])
|
||||||
|
else:
|
||||||
|
raise ValueError("Could not get latest github version")
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Generator, Optional
|
||||||
|
|
||||||
from .availability_api import WaybackMachineAvailabilityAPI
|
from .availability_api import WaybackMachineAvailabilityAPI
|
||||||
from .cdx_api import WaybackMachineCDXServerAPI
|
from .cdx_api import WaybackMachineCDXServerAPI
|
||||||
@@ -19,35 +20,37 @@ the older interface code.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Url:
|
class Url(object):
|
||||||
def __init__(self, url, user_agent=DEFAULT_USER_AGENT):
|
def __init__(self, url: str, user_agent: str = DEFAULT_USER_AGENT) -> None:
|
||||||
self.url = url
|
self.url = url
|
||||||
self.user_agent = str(user_agent)
|
self.user_agent = str(user_agent)
|
||||||
self.archive_url = None
|
self.archive_url: Optional[str] = None
|
||||||
self.timestamp = None
|
self.timestamp: Optional[datetime] = None
|
||||||
self.wayback_machine_availability_api = WaybackMachineAvailabilityAPI(
|
self.wayback_machine_availability_api = WaybackMachineAvailabilityAPI(
|
||||||
self.url, user_agent=self.user_agent
|
self.url, user_agent=self.user_agent
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
if not self.archive_url:
|
if not self.archive_url:
|
||||||
self.newest()
|
self.newest()
|
||||||
return self.archive_url
|
return str(self.archive_url)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self) -> int:
|
||||||
td_max = timedelta(
|
td_max = timedelta(
|
||||||
days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999
|
days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999
|
||||||
)
|
)
|
||||||
|
|
||||||
if not self.timestamp:
|
if not isinstance(self.timestamp, datetime):
|
||||||
self.oldest()
|
self.oldest()
|
||||||
|
|
||||||
if self.timestamp == datetime.max:
|
if not isinstance(self.timestamp, datetime):
|
||||||
|
raise TypeError("timestamp must be a datetime")
|
||||||
|
elif self.timestamp == datetime.max:
|
||||||
return td_max.days
|
return td_max.days
|
||||||
|
else:
|
||||||
|
return (datetime.utcnow() - self.timestamp).days
|
||||||
|
|
||||||
return (datetime.utcnow() - self.timestamp).days
|
def save(self) -> "Url":
|
||||||
|
|
||||||
def save(self):
|
|
||||||
self.wayback_machine_save_api = WaybackMachineSaveAPI(
|
self.wayback_machine_save_api = WaybackMachineSaveAPI(
|
||||||
self.url, user_agent=self.user_agent
|
self.url, user_agent=self.user_agent
|
||||||
)
|
)
|
||||||
@@ -58,13 +61,13 @@ class Url:
|
|||||||
|
|
||||||
def near(
|
def near(
|
||||||
self,
|
self,
|
||||||
year=None,
|
year: Optional[int] = None,
|
||||||
month=None,
|
month: Optional[int] = None,
|
||||||
day=None,
|
day: Optional[int] = None,
|
||||||
hour=None,
|
hour: Optional[int] = None,
|
||||||
minute=None,
|
minute: Optional[int] = None,
|
||||||
unix_timestamp=None,
|
unix_timestamp: Optional[int] = None,
|
||||||
):
|
) -> "Url":
|
||||||
|
|
||||||
self.wayback_machine_availability_api.near(
|
self.wayback_machine_availability_api.near(
|
||||||
year=year,
|
year=year,
|
||||||
@@ -77,22 +80,24 @@ class Url:
|
|||||||
self.set_availability_api_attrs()
|
self.set_availability_api_attrs()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def oldest(self):
|
def oldest(self) -> "Url":
|
||||||
self.wayback_machine_availability_api.oldest()
|
self.wayback_machine_availability_api.oldest()
|
||||||
self.set_availability_api_attrs()
|
self.set_availability_api_attrs()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def newest(self):
|
def newest(self) -> "Url":
|
||||||
self.wayback_machine_availability_api.newest()
|
self.wayback_machine_availability_api.newest()
|
||||||
self.set_availability_api_attrs()
|
self.set_availability_api_attrs()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_availability_api_attrs(self):
|
def set_availability_api_attrs(self) -> None:
|
||||||
self.archive_url = self.wayback_machine_availability_api.archive_url
|
self.archive_url = self.wayback_machine_availability_api.archive_url
|
||||||
self.JSON = self.wayback_machine_availability_api.JSON
|
self.JSON = self.wayback_machine_availability_api.JSON
|
||||||
self.timestamp = self.wayback_machine_availability_api.timestamp()
|
self.timestamp = self.wayback_machine_availability_api.timestamp()
|
||||||
|
|
||||||
def total_archives(self, start_timestamp=None, end_timestamp=None):
|
def total_archives(
|
||||||
|
self, start_timestamp: Optional[str] = None, end_timestamp: Optional[str] = None
|
||||||
|
) -> int:
|
||||||
cdx = WaybackMachineCDXServerAPI(
|
cdx = WaybackMachineCDXServerAPI(
|
||||||
self.url,
|
self.url,
|
||||||
user_agent=self.user_agent,
|
user_agent=self.user_agent,
|
||||||
@@ -107,12 +112,12 @@ class Url:
|
|||||||
|
|
||||||
def known_urls(
|
def known_urls(
|
||||||
self,
|
self,
|
||||||
subdomain=False,
|
subdomain: bool = False,
|
||||||
host=False,
|
host: bool = False,
|
||||||
start_timestamp=None,
|
start_timestamp: Optional[str] = None,
|
||||||
end_timestamp=None,
|
end_timestamp: Optional[str] = None,
|
||||||
match_type="prefix",
|
match_type: str = "prefix",
|
||||||
):
|
) -> Generator[str, None, None]:
|
||||||
if subdomain:
|
if subdomain:
|
||||||
match_type = "domain"
|
match_type = "domain"
|
||||||
if host:
|
if host:
|
||||||
|
Reference in New Issue
Block a user