sunrise-sunet: commit basic data and scraping code

This commit is contained in:
Thomas 2023-01-23 20:26:50 +00:00
parent 897d859d21
commit 363051c35e
17 changed files with 2158 additions and 6 deletions

View file

@ -1,6 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100
}

View file

@ -37,6 +37,7 @@
"type": "module",
"dependencies": {
"date-fns": "^2.29.2",
"just-shuffle": "^4.2.0",
"leaflet": "^1.8.0",
"mdsvex": "^0.10.5",
"mongodb": "^4.8.1",

6
prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": true
}

View file

@ -0,0 +1 @@
environment_variables.py

View file

@ -0,0 +1,69 @@
import json
import datetime
import utils
import photos
class DailyPhoto:
def __init__(self, date: datetime.date, photo: photos.SunriseOrSunsetPhoto):
self.date = date
self.photo = photo
def as_dict(self) -> dict:
return {
"date": self.date.strftime("%Y-%m-%d"),
"photo": self.photo.as_json()
}
class DailyPhotoSet:
def __init__(self, start_date: datetime.date = datetime.date(2023, 1, 21)):
self._date = start_date
self._photos = []
def add_photo(self, photo: photos.SunriseOrSunsetPhoto):
self._photos.append(DailyPhoto(self._date, photo))
self.photos.sort(key=lambda photo: photo.photo.id)
self._date += datetime.timedelta(days=1)
@property
def photos(self) -> list[DailyPhoto]:
return list(sorted(self._photos, key=lambda photo: photo.date))
@property
def current_date(self) -> datetime.date:
return self._date;
def as_json(self) -> str:
return json.dumps({
"photos": [photo.as_dict() for photo in self.photos]
})
def get_all_photos() -> list[photos.SunriseOrSunsetPhoto]:
file_names: list[str] = ["data/all_photos-0.json", "data/all_photos-1.json"]
sunrise_or_sunset_photos = photos.SunriseOrSunsetPhotoSet.from_no_data();
for file_name in file_names:
with open(utils.make_relative_file_name(file_name), "r") as infile:
photo_set = photos.SunriseOrSunsetPhotoSet.from_json(infile.read())
sunrise_or_sunset_photos.add_photos(photo_set.unique_photos)
return sunrise_or_sunset_photos.photos_sorted_by_id
def main():
print("Creating daily photo mapping...")
all_photos = get_all_photos()
print("Found {} photos".format(len(all_photos)))
daily_photos = DailyPhotoSet()
for photo in all_photos:
daily_photos.add_photo(photo)
with open(utils.make_relative_file_name('data/daily-photos.json'), 'w') as outfile:
outfile.write(daily_photos.as_json())
main()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,37 @@
import environment_variables
import unsplash_api_gateway
import photos
import utils
import json
def main():
"""Get sunrise and sunset images from Unsplash API."""
api = unsplash_api_gateway.UnsplashApi(environment_variables.UNSPLASH_ACCESS_KEY, environment_variables.UNSPLASH_SECRET_KEY)
sunrise_images = api.search_results_for_query(query="sunrise", page_size=100, page_number=5)
sunset_images = api.search_results_for_query(query="sunset", page_size=100, page_number=5)
unfiltered_length = len(sunrise_images.unfiltered_results) + len(sunset_images.unfiltered_results)
filtered_length = len(sunrise_images.results) + len(sunset_images.results)
print(f"Found {unfiltered_length} unfiltered images...")
print(f"Found {filtered_length} filtered images")
all_photos = photos.SunriseOrSunsetPhotoSet.from_unsplash_search_results(sunrise_images, sunset_images)
file_name = utils.make_relative_file_name("data/all_photos.json")
print(f"Found {len(all_photos.photos)} photos")
file_contents = json.loads(open(file_name, "r").read())
with open(file_name, "w") as outfile:
photos_in_file = [photos.SunriseOrSunsetPhoto.from_json(photo) for photo in file_contents["photos"]]
print(f"Found {len(photos_in_file)} photos in {file_name}...")
photos_without_opposite_daytime = [photo for photo in photos_in_file if not photo.does_description_contain_opposite_daytime]
print(f"Found {len(photos_without_opposite_daytime)} photos without opposite daytime...")
all_photos.add_photos(photos_without_opposite_daytime)
print(f"Writing {len(all_photos.photos)} photos to {file_name}")
outfile.write(all_photos.as_json())
main()

View file

@ -0,0 +1,102 @@
import json
import unsplash_search_results
class SunriseOrSunsetPhoto:
def __init__(
self,
unsplash_search_result: unsplash_search_results.UnsplashSearchResult,
sunrise_or_sunset: str
):
self.id = unsplash_search_result.id
self.description = unsplash_search_result.description
self.username = unsplash_search_result.username
self.username_url = unsplash_search_result.username_url
self.small_url = unsplash_search_result.small_url
self.sunrise_or_sunset = sunrise_or_sunset
@staticmethod
def from_json(data: dict) -> "SunriseOrSunsetPhoto":
unsplash_search_result = unsplash_search_results.UnsplashSearchResult(
data={
"id": data["id"],
"description": data["description"],
"user": {
"username": data["username"],
"links": {
"html": data["username_url"]
},
},
"urls": {
"small": data["small_url"]
},
}
)
return SunriseOrSunsetPhoto(
unsplash_search_result=unsplash_search_result,
sunrise_or_sunset=data["sunrise_or_sunset"],
)
@property
def does_description_contain_opposite_daytime(self) -> bool:
opposite_word = "sunrise" if self.sunrise_or_sunset == "sunset" else "sunset"
return opposite_word in self.description.lower()
def as_json(self) -> object:
return {
"id": self.id,
"description": self.description,
"username": self.username,
"username_url": self.username_url,
"small_url": self.small_url,
"sunrise_or_sunset": self.sunrise_or_sunset
}
class SunriseOrSunsetPhotoSet:
def __init__(self, photos: list[SunriseOrSunsetPhoto]):
self.photos = photos
self.unique_photos = self.get_unique_photos(photos)
@staticmethod
def from_no_data() -> "SunriseOrSunsetPhotoSet":
return SunriseOrSunsetPhotoSet(photos=[])
@staticmethod
def from_unsplash_search_results(sunrise_images: unsplash_search_results.UnsplashSearchResultSet, sunset_images: unsplash_search_results.UnsplashSearchResultSet):
photos = []
photos.extend([SunriseOrSunsetPhoto(result, "sunrise") for result in sunrise_images.results])
photos.extend([SunriseOrSunsetPhoto(result, "sunset") for result in sunset_images.results])
return SunriseOrSunsetPhotoSet(photos)
@staticmethod
def from_json(json_string: str) -> "SunriseOrSunsetPhotoSet":
loaded_json = json.loads(json_string)
photos = [SunriseOrSunsetPhoto.from_json(photo) for photo in loaded_json["photos"]]
return SunriseOrSunsetPhotoSet(photos)
def add_photos(self, photos: list[SunriseOrSunsetPhoto]):
self.photos.extend(photos)
self.unique_photos = self.get_unique_photos(self.photos)
def get_unique_photos(self, photos: list[SunriseOrSunsetPhoto]) -> list[SunriseOrSunsetPhoto]:
"""
Some results are duplicates (identified by `id`). This could mean they're
not definitely a sunrise or sunset, so neither image should be included.
"""
all_ids: list[str] = list(map(lambda photo: photo.id, photos))
unique_results = []
for result in photos:
is_id_present_once: bool = 1 == len(list(filter(lambda id: id == result.id, all_ids)))
if is_id_present_once:
unique_results.append(result)
return unique_results
def as_json(self) -> str:
return json.dumps({
"photos": list(map(lambda photo: photo.as_json(), self.unique_photos))
})
@property
def photos_sorted_by_id(self) -> list[SunriseOrSunsetPhoto]:
return sorted(self.unique_photos, key=lambda photo: photo.id)

View file

@ -0,0 +1,11 @@
# Sunrise or Sunset Images
You need to create an Unsplash developer account with API key ([link](https://unsplash.com/developers)).
To authenticate calls to the Unsplash API, you need to create a file called `environment_variables.py` to store your access and secret key:
```py
# environment_variables.py
UNSPLASH_ACCESS_KEY = "access_key"
UNSPLASH_SECRET_KEY="secret_key"
```

View file

@ -0,0 +1,25 @@
import requests
import unsplash_search_results
class UnsplashApi:
def __init__(self, access_key, secret_key):
"""Initialize the Unsplash API wrapper."""
self.access_key = access_key
self.secret_key = secret_key
def search_results_for_query(self, query, page_size=1, page_number=1):
"""Search for photos."""
response = requests.get(
"https://api.unsplash.com/search/photos",
params={
"query": query,
"page": page_number,
"per_page": page_size
},
headers={
"Authorization": f"Client-ID {self.access_key}"
}
)
return unsplash_search_results.UnsplashSearchResultSet(response.json(), query)

View file

@ -0,0 +1,61 @@
class UnsplashSearchResult:
def __init__(self, data: dict):
self.id = data["id"]
self._description = data["description"] or "No description"
self.username = data["user"]["username"]
self.username_url = data["user"]["links"]["html"]
self.small_url = data["urls"]["small"]
self.json = data
def __str__(self):
return f"{self.description} : {self.small_url} by {self.username}"
@staticmethod
def from_json(data: dict) -> "UnsplashSearchResult":
formatted_data = {
"id": data["id"],
"description": data["description"],
"user": {
"username": data["username"],
"links": {
"html": data["username_url"],
},
},
}
return UnsplashSearchResult(data=formatted_data)
@property
def description(self):
return self._description
def as_json(self):
return {
"id": self.id,
"description": self.description,
"username": self.username,
"username_url": self.username_url,
"small_url": self.small_url
}
class UnsplashSearchResultSet:
def __init__(self, json, sunrise_or_sunset: str):
self.total = json["total"]
self.total_pages = json["total_pages"]
self.json = json
self.unfiltered_results = [UnsplashSearchResult(photo) for photo in json["results"]]
self.results = self.filter_out_photos_with_word_in_description("sunrise") if sunrise_or_sunset == "sunrise" else self.filter_out_photos_with_word_in_description("sunset")
self.sunrise_or_sunset = sunrise_or_sunset
def filter_out_photos_with_word_in_description(self, word: str):
lower_word = word.lower()
return [search_result for search_result in self.unfiltered_results if lower_word not in search_result.description.lower()]
def as_json(self):
return {
"total": self.total,
"sunrise_or_sunset": self.sunrise_or_sunset,
"total_pages": self.total_pages,
"results": [result.as_json() for result in self.results]
}

View file

@ -0,0 +1,5 @@
import os
def make_relative_file_name(path: str) -> str:
dirname = os.path.dirname(__file__)
return os.path.join(dirname, path)

View file

@ -0,0 +1,14 @@
export interface SunriseOrSunsetPhotoSet {
total: number
total_pages: number
search_term: string
results: SunriseOrSunsetPhoto[]
}
export interface SunriseOrSunsetPhoto {
id: string
description: string
username: string
username_url: string
small_url: string
}

View file

@ -0,0 +1,10 @@
import type { LoadEvent } from '@sveltejs/kit'
export const GET = async (_event: LoadEvent) => {
return Promise.resolve({
status: 200,
body: {
photos: [],
},
})
}

View file

@ -0,0 +1,86 @@
<script lang="ts">
import { writable } from 'svelte/store';
const pictures: {
imageUrl: string;
daytime: 'sunrise' | 'sunset';
}[] = [
{
imageUrl: '/example',
daytime: 'sunrise'
},
{
imageUrl: '/examplele',
daytime: 'sunset'
}
];
const pictureStore = writable(pictures[0]);
const guessHistory = writable({
correct: 0,
incorrect: 0,
total: 0
});
function onOptionSelected(option: 'sunrise' | 'sunset') {
$guessHistory.total += 1;
if (option === $pictureStore.daytime) {
$guessHistory.correct += 1;
} else {
$guessHistory.incorrect += 1;
}
console.log(option);
}
</script>
<h1>Sunrise, Sunset?</h1>
<section class="picture">
<img src={$pictureStore.imageUrl} alt="Sunrise or Sunset?" />
</section>
<section class="options">
<div class="options__buttons-container">
<button class="options__button" on:click={() => onOptionSelected('sunrise')}>Sunrise</button>
<button class="options__button" on:click={() => onOptionSelected('sunset')}>Sunset</button>
</div>
</section>
<section class="score">
{#if $guessHistory.total > 0}
<p class="score__text">
You've guessed correctly {Number($guessHistory.correct / $guessHistory.total)}% of the time
</p>
{:else}
<p class="score__text">You've not guessed yet.</p>
{/if}
</section>
<style lang="scss" type="text/postcss">
.options {
width: 100%;
display: flex;
justify-content: center;
}
.options__buttons-container {
display: grid;
grid-template-columns: 50% 50%;
gap: 12px;
}
.options__button {
background-color: #fff;
border: 1px solid #000;
border-radius: 4px;
padding: 12px;
font-size: 1.2rem;
cursor: pointer;
}
.options__button:hover {
background-color: #000;
color: #fff;
}
</style>

View file

@ -3,6 +3,7 @@ import { sveltekit } from '@sveltejs/kit/vite';
/** @type {import('vite').UserConfig} */
const config = {
plugins: [sveltekit()],
resolve: {}
};
export default config;

View file

@ -1427,6 +1427,11 @@ json-stable-stringify-without-jsonify@^1.0.1:
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
just-shuffle@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/just-shuffle/-/just-shuffle-4.2.0.tgz#795c09dbac1f9e94e797efd10a7ef537363c8903"
integrity sha512-/dDmNseAWLf3XkFY9xf3/BdQoiy27LNUy/7uG4zdSAX526nIHMYPYeJ4pN4lT1/pgNEX8XCXPtUB6gJqTpBEng==
kleur@^4.1.4, kleur@^4.1.5:
version "4.1.5"
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"