ToCode
الذهاب إلى القناة على Telegram
1 420
المشتركون
لا توجد بيانات24 ساعات
لا توجد بيانات7 أيام
-530 أيام
أرشيف المشاركات
1 420
קונספציות
שני סיפורים לא קשורים על פערי ציפיות שאכלו לי יותר מדי זמן השבוע (בטח מספיק בשביל להופיע פה בבלוג).
הראשון הוא השילוב בין דוקר לחומת האש ufw. במשפט שני אלה לא עובדים טוב יחד. כשמפעילים ufw כדי לחסום חיבורים נכנסים הוא לא חוסם חיבורים שממופים לקונטיינרים בדוקר. במילים אחרות אם הפעלתם קונטיינר שמקשיב על פורט 8080:
docker run -dit --name my-apache-app -p 8080:80 -v "$PWD":/usr/local/apache2/htdocs/ httpd:2.4
ואז תפעילו ufw כדי לחסום חיבורים מבחוץ לפורט 8080, החיבורים עדיין ייכנסו ויגיעו לקונטיינר.
אני מודה כשראיתי את ההתנהגות הזאת לא האמנתי. הייתי בטוח שאני עושה משהו לא בסדר במקום אחר. אחרי כמה שעות הלכתי לגוגל ומצאתי שהסיפור מתועד למשל כאן:
https://blog.jarrousse.org/2023/03/18/how-to-use-ufw-firewall-with-docker-containers/
וכאן:
https://www.baeldung.com/linux/docker-container-published-port-ignoring-ufw-rules
וכאן:
https://www.howtogeek.com/devops/how-to-use-docker-with-a-ufw-firewall/
הסיפור השני הוא על JanusGraph ויצירת אינדקס ייחודי בגרף. בעמוד התיעוד הראשי הם משתפים את הפקודה שיוצרת אינדקס ייחודי:
index = mgmt.buildIndex('byConsistentName', Vertex.class).addKey(name).unique().buildCompositeIndex()
רק מה, זה לא עובד. בשביל ליצור אינדקס ייחודי שגם מוודא שדברים נוצרים בצורה ייחודית צריך לבקש ממנו להשתמש גם במנגנון הנעילה או במילים אחרות:
index = mgmt.buildIndex('byConsistentName', Vertex.class).addKey(name).unique().buildCompositeIndex()
mgmt.setConsistency(index, ConsistencyModifier.LOCK)
וגם זה מתועד יפה כשיודעים איפה לחפש:
https://docs.janusgraph.org/advanced-topics/eventual-consistency/
גם אם שני הסיפורים הם על שתי טכנולוגיות שונות יש קשר חזק ביניהם. בשני המקרים חוסר תשומת לב יצרה בעיות שבהמשך הרבה יותר קשה לתקן. בכתיבת קוד שימו לב להשתמש תמיד במנה גדושה של ספקנות בריאה ותמיד לבחון את ההנחות שלכם, גם כשדברים נראים מובנים מאליהם.1 420
כמה זה באמת עולה?
המחיר האמיתי של בעיה כולל לא רק את הערך שאנחנו מפסידים בזה שלא פותרים אותה, אלא גם את כל הזמן האבוד שאנחנו שורפים על ניסיונות נוספים להתמודד.
שרת יציב שמחובר למערכת ניטור שגיאות יאפשר לי לראות מיד כשיש בעיה וגם לחפור במדדים של השרת כדי להבין מה נשבר. אבל אם השרת לא בנוי כמו שצריך גם יהיו בו יותר בעיות, וגם כשכבר יש בעיה אין לי דרך טובה לחקור אותה ולתקן את הדבר האמיתי ששבור, ואז צריך לשרוף שעות בניסיונות עד שמגיעים למשהו.
רוב הזמן אנחנו יכולים לראות את הבעיות שעוצרות אותנו, אבל אין לנו זמן או ידע מתאים כדי לפתור אותן או שיש בעיות שנראות יותר דחופות. ולכן האתגר שלנו (כמפתחים, כיזמים, כמנהלים) הוא לזהות את העלות האמיתית של בעיה. בעיה שעולה יום עבודה פעם בחודש כל חודש היא דחופה, גם אם היום הוא לא היום של השריפה.
1 420
טיפ ריאקט: עדיף לוותר על טרנרי בתוך JSX
יותר מדי פעמים אני רואה קוד עם כוונות טובות שלא עומד במבחן הזמן. במקרה של ריאקט תבנית מאוד בעייתית היא הכנסת לוגיקה לתוך ה JSX. זה מתחיל פשוט עם איזה סימן שאלה נקודותיים:
function App() {
const [text, setText] = useState(0);
return (
<div>
<button onClick={() => setText(t => (t + 1) % 2)}>Toggle</button>
{text == 0 ? <Text1 /> : <Text2 />}
</div>
)
}
אבל מהר מאוד הופך למפלצת.
מה עושים במקום? מנגנון קל הוא להוציא את הקוד המכוער לקומפוננטה נפרדת שחיה רק בשביל הלוגיקה הזאת. החלטנו שצריך להתלבט איזה קומפוננטה להציג? בואו נעביר את זה לקומפוננטה נפרדת ונקבל:
function Text({id}) {
if (id === 0) {
return <Text1 />
} else {
return <Text2 />
}
}
function App() {
const [text, setText] = useState(0);
return (
<div>
<button onClick={() => setText(t => (t + 1) % 2)}>Toggle</button>
<Text id={text} />
</div>
)
}
בשלב הבא אם נצטרך מנגנון יותר גנרי (למשל כי נצטרך שוב לבחור בין כמה קומפוננטות) נוכל להפוך את Text לפונקציה כללית ונקבל:
function Text1({color}) {
return <p style={{color}}>Text 1</p>
}
function Text2({color}) {
return <p style={{color}}>Text 2</p>
}
const Toggle = (...components) => (props) => {
const cls = components[props.id]
return React.createElement(cls, props);
}
const Text = Toggle(Text1, Text2);
function App() {
const [text, setText] = useState(0);
return (
<div>
<button onClick={() => setText(t => (t + 1) % 2)}>Toggle</button>
<Text id={text} color="red" />
</div>
)
}1 420
חלומות על PGlite
הפעלתי היום קוד טייפסקריפט שהתחבר לבסיס נתונים PGlite, יצר טבלה, הכניס נתונים ושלף אותם חזרה. החלק המלהיב הוא שלא היה לי בסיס נתונים מותקן. זה הקוד וצריך רק deno run בשביל להריץ:
import { PGlite } from "npm:@electric-sql/pglite"
async function main() {
const db = new PGlite()
await db.query("create table test(x integer, y integer);");
await db.query("insert into test values(1, 1);");
await db.query("insert into test values(2, 2);");
const result = await db.query("select * from test");
console.log(result);
}
main();
הפרויקט עדיין בשלבי פיתוח וידרוש עוד עבודה עד שנקבל תמיכה ב ORM-ים אבל כבר אפשר לחלום על כמה שיפורים משמעותיים בפיתוח שהוא יוכל להכניס לאקוסיסטם של JavaScript בצד שרת-
1. מהירות - כי אם בסיס הנתונים רץ באותו תהליך כמו השרת אז יותר זול להוציא קריאות לבסיס הנתונים.
2. שאילתות יותר פשוטות - המשך של הסעיף הקודם, כי אם בסיס הנתונים רץ אצלי באותו תהליך אני יכול להוציא גם 20 שאילתות ויכול לוותר על JOIN-ים או חיבורים מורכבים בין טבלאות.
3. בדיקות מהירות ומקביליות - כי עכשיו כל בדיקה יכולה ליצור לעצמה את בסיס הנתונים בזיכרון מתוך נתוני Seed קבועים.
4. פורטינג יותר מהיר לאפליקציות Offline First - כי אפשר לקחת את אותן שאילתות פוסטגרס שכבר יש לנו ופשוט להריץ הכל בדפדפן.
כמה מהדברים האלה יעבדו ומתי עדיין מוקדם לנחש. כנראה שפורטינג ל Offline First יהיה הדבר הראשון, אחרי זה נתחיל להשתמש ב PGlite בתשתית הבדיקות עד שיהיה מספיק יציב להריץ את כל השאילתות של המערכת האמיתית ולאט לאט יהיו אמיצים שיבחרו אותו בתור בסיס נתונים מלא. או שלא. בכל מקרה זה פרויקט ששווה לעקוב אחריו.1 420
ממשק אחד שעושה הכל
צריכים לכתוב שחקן מחשב לאיקס עיגול (או אולי לשחמט)?
ליצור תוכנית אימונים מותאמת אישית לחדר כושר, לפי רשימת אילוצים ורצונות של משתמש?
לזהות באיזה שפה הקובץ? או לתרגם את הטקסט לשפה אחרת?
לחפש מתכונים שמתאימים לרשימת מרכיבים שיש למשתמש במקרר כרגע?
לפענח קוד QR בתמונה?
כשנצטרך לפתור כל אחת מהבעיות ברשימה למעלה (ובעיות רבות נוספות), רובנו נחפש או ננסה לחשוב על אלגוריתם - נדמיין את הקלט, הפלט ואופן החישוב. אנחנו רוצים לדעת מה הקוד עושה ולהיות מסוגלים לתקן את הפיתרון ולשפר אותו.
אבל אפשר כבר לדמיין עתיד מסוג אחר.
באותו עתיד אפשר לדמיין איך במקום לכתוב את הקוד לפיתרון הבעיה אנחנו נכתוב קוד שינגיש את הבעיה לאיזה "אורקל" כמו Chat GPT ויפענח את תשובתו. תיקוני באגים יהיו בסך הכל Prompt Engineering ובדיקות אוטומטיות יהיו רשימה של פרומפטים מוכנים מראש והתוצאות שאנחנו מצפים לקבל מהמכונה. ההזדמנות כבר שם לקצר תהליכי פיתוח בצורה משמעותית, ועם הזמן היא תיהפך יותר ברורה.
1 420
חדש באתר - סידרת useEffect
אין ספק ש useEffect הוא ה Hook המבלבל ביותר בריאקט. מצד אחד אפשר לעשות איתו המון דברים וגם יש המון מקרים בהם באמת צריך אותו, אבל מצד שני קל מאוד להשתמש בו לא נכון ולכן רוב האנשים נתקלים בבאגים מוזרים כשמתחילים לכתוב אפקטים בקומפוננטות.
עוד כשכתבו לראשונה את המנגנון דן אברמוב דן עליו באריכות בבלוג שלו:
https://overreacted.io/a-complete-guide-to-useeffect/
וזה עזר, אבל רק במידה מסוימת כי בסוף עד שלא מנסים את הדברים על קוד אמיתי קשה לראות את הבעיות.
בשנים שעברו מאז ש useEffect יצא נצבר לא מעט ידע על איך להשתמש בו נכון, למה לשים לב ומהם הבאגים הכי נפוצים שניתקל בהם. למרות שלימדתי את useEffect בקורס ריאקט השיעורים שם היו ממוקדים במה אפשר לבנות עם useEffect הרבה יותר מאשר במה אפשר לשבור איתו.
השבוע בעקבות כמה שאלות מתלמידים ישבתי לסדר את השיעורים האלה בקורס ולהקליט מחדש 3 סרטים על useEffect בגירסאות ארוכות ומפורטות (שעה של וידאו על useEffect אמרתם? יאללה קיבלתם). הפעם לקחתי כל דוגמה והראיתי את כל הטעויות שאפשר לעשות ואיך לזהות כל טעות.
אם יש לכם שעה פנויה ומנוי לאתר, ואתם כותבים ריאקט ביום יום, אני ממליץ להעיף מבט בסידרה המחודשת בקישור: https://www.tocode.co.il/bundles/react/toc שיעורים 25-27.
וכמו תמיד אם יש נושאים נוספים שהייתם רוצים לראות בקורסים כאן אל תתביישו לכתוב, אפשר דרך מסך צרו קשר כאן באתר.
1 420
בואו נתקן את cycle בפייתון
הפונקציה cycle מתוך המודול itertools בפייתון לוקחת אוסף של דברים ומחזירה איטרטור אינסופי שכל פעם מחזיר את הדבר הבא מתוך הרצף במעגל. לדוגמה אפשר להשתמש בה עם המחרוזת abc באופן הבא:
print(list(itertools.islice(itertools.cycle("abc"), 10)))
ולקבל את התוצאה:
['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b', 'c', 'a']
אבל לסייקל יש בעיית ממשק - היא יכולה לקבל רק פרמטר אחד. לכן אם במקום רשימת האותיות הייתי רוצה להעביר רשימה של מספרים הייתי צריך לכתוב:
print(list(itertools.islice(itertools.cycle([1, 2, 3]), 10)))
וזה כבר מעייף לכתוב גם סוגריים עגולים וגם סוגריים מרובעים. מה עושים? מתקנים. אני לא שובר את הממשק של cycle אבל אני כן יכול לכתוב פונקציה עוטפת שתעשה לי חיים יותר קלים. הנה הקוד:
def easy_cycle(*args):
match args:
case [col] if hasattr(col, '__iter__'):
return itertools.cycle(col)
case _:
return itertools.cycle(args)
הפונקציה לוקחת רשימה של דברים ובודקת את הפרטים של הדבר הראשון שהיא קיבלה. אם זה משהו שאפשר לעשות עליו איטרציה נעביר אותו ל cycle בלי שינויים. בכל מצב אחר נעביר את רשימת הדברים שקיבלנו ל cycle.
התוצאה פשוטה ועכשיו הממשק מסתדר גם כשאני שוכח סוגריים מרובעים:
print(list(itertools.islice(easy_cycle("abc"), 10)))
print(list(itertools.islice(easy_cycle("a", "b", "c"), 10)))
print(list(itertools.islice(easy_cycle(["a", "b", "c"]), 10)))
נ.ב. היה נחמד אם מנגנון ה Type Hints של פייתון היה מאפשר לי להגדיר שערך ההחזר של הפונקציה שכתבתי זהה לערך ההחזר של הפונקציה itertools.cycle. בינתיים אפשר להעתיק חתימות או לוותר על ה Type Hint במקרה כזה.1 420
def toColumns(matrix: Map[(Long, Long), Char]): List[String] =
val lastColumn = matrix
.keys
.collect { (row, column) => column }
.max
.toInt
0.to(lastColumn)
.map { col =>
matrix
.keys
.filter { case (_, c) => col == c }
.toList
.sortBy(_._1)
.map(matrix)
.mkString
}.toList
וזה מספיק בשביל החלק הראשון. הפיתרון עם כל פונקציות העזר שכבר כתבנו הוא בסך הכל:
@main
def day13part1(): Unit =
Source
.fromResource("day13.txt")
.getLines()
.toParagraphs
.map(toMatrix)
.toList
.map { m => (toLines(m), toColumns(m)) }
.collect { case (rows, column) =>
findMirrorInList(rows) * 100 + findMirrorInList(column)
}
.sum
.pipe(println)
לוקחים כל מטריצה, מוציאים ממנה את רשימת השורות ורשימת העמודות, מחפשים מראות ומדפיסים את הסכום.
פיתרון חלק 2
החלק השני לא נשמע מסובך אבל בסוף כן דרש כתיבה של לא מעט קוד חדש. קודם כל הפונקציה swap שהופכת סולמית לנקודה ולהיפך:
def swap(ch: Char): Char =
if (ch == '.') { '#' } else { '.' }
ועכשיו לפיתרון - לוקחים את כל המטריצות שלנו, לכל מטריצה לוקחים את כל המפתחות ולכל אינדקס יוצרים רשימה חדשה של מטריצות, כך שמטריצה בגודל 9 תאים הפכה עכשיו ל 9 מטריצות בגודל 9 תאים. כל אחת מהמטריצות החדשות היא בעצם היפוך התו באינדקס מסוים במטריצה המקורית. עכשיו אפשר לרוץ על כל המטריצות החדשות ולחפש בהן מראות, רק צריך לשים לב לא לבחור את המראה של המטריצה המקורית. הלולאה היא בעצם לולאה בתוך לולאה, כלומר רצים על כל המטריצות, ועבור כל מטריצה יוצרים n מטריצות חדשות ורצים עליהן. זה נשמע מסורבל אבל המחשב הסתדר יפה עם האתגר.
בקיצור זה הקוד של החלק השני:
@main
def day13part2(): Unit =
Source
.fromResource("day13.txt")
.getLines()
.toParagraphs
.map(toMatrix)
.toList
.map { m => (m, toLines(m), toColumns(m)) }
.collect { case (m, rows, columns) =>
val horizontalMirror = findMirrorInList(rows)
val verticalMirror = findMirrorInList(columns)
m
.keys
.map { k => m.updatedWith(k) { case Some(ch) => Some(swap(ch)) } }
.map { m => (findMirrorInList(toLines(m), Set(horizontalMirror)), findMirrorInList(toColumns(m), Set(verticalMirror))) }
.find { i => i != (0, 0) }
.map {
case (0, altVertical) => altVertical
case (altHorizontal, 0) => altHorizontal * 100
}
}
.map(_.getOrElse(0))
.sum
.pipe(println)
אני די בטוח שאפשר לקחת את החלק המרכזי לפונקציית עזר, אבל אחרי שהרצתי והגעתי לתשובה הנכונה הרגשתי שכתבתי מספיק קוד בשביל התרגיל הזה ועדיף להיפרד ממנו כאן.
רעיונות איך לשפר? פיתרונות בשפות אחרות? מוזמנים לשתף בתגובות או בטלגרם.1 420
פיתרון Advent Of Code 2023 יום 13 בסקאלה
כן אנחנו ממשיכים בסידרה הזאת ולא להאמין שעברנו כבר חצי מהאתגר והכל בשפת סקאלה. המטרה של הפוסטים האלה היא כמובן לדבר קצת על סקאלה אבל גם לתת לכם מוטיבציה וכיוון לפתור את זה בשפות אחרות שאתם לומדים. מוכנים? בואו נצלול לפרטים.
האתגר
הקלט שלנו היום הוא רשימה של מטריצות עם שורה ריקה בין כל מטריצה לזו שאחריה לדוגמה:
#.##..##.
..#.##.#.
##......#
##......#
..#.##.#.
..##..##.
#.#.##.#.
#...##..#
#....#..#
..##..###
#####.##.
#####.##.
..##..###
#....#..#
כל מטריצה מייצגת תמונה עם מראה באמצע, המראה יכולה להיות בקו אופקי או אנכי ואנחנו מזהים אותה כי הדברים שמשני הצדדים של המראה זהים. לדוגמה בציור הראשון המראה היא בין העמודה החמישית לשישית:
123456789
><
#.##..##.
..#.##.#.
##......#
##......#
..#.##.#.
..##..##.
#.#.##.#.
><
123456789
אפשר לראות איך מה שמשמאל לעמודה הוא תמונת ראי של מה שמימין לה, חוץ מהנקודות שנמצאות "רחוק מדי" למשל העמודה הראשונה שהיא לא בתמונה. בתמונה השניה המראה היא אופקית בין השורה הרביעית לחמישית:
1 #...##..# 1
2 #....#..# 2
3 ..##..### 3
4v#####.##.v4
5^#####.##.^5
6 ..##..### 6
7 #....#..# 7
האתגר הראשון שלנו הוא למצוא איפה המראה ואז לסכום את המיקומים של כל המראות (אם היא אופקית מכפילים ב 100, אנוכית נשארת כמו שהיא).
בחלק השני של התרגיל אנחנו מגלים שאפשר להחליף פיקסל אחד (מנקודה לסולמית או מסולמית לנקודה) ולקבל מראה במקום אחר, ומבקשים מאיתנו למצוא את כל המקומות החדשים של המראות.
פיתרון לחלק הראשון
הפונקציה המרכזית בתרגיל מקבלת רשימה של מחרוזות ומחפשת את נקודת המראה ברשימה, כלומר נקודה שהמחרוזות משמאלה הן תמונת ראי של מה שמימינה. הלוגיקה לא מסובכת אבל צריך לשים לב ולטפל בנפרד במקרה שהנקודה קרובה יותר לתחילת הרשימה או לסוף הרשימה:
def findMirrorInList(items: List[String], except: Set[Int] = Set()): Int =
1.until(items.length).map { i =>
if (i <= items.length / 2) {
items.slice(0, i) == items.slice(i, i * 2).reverse
} else {
val distanceFromEnd = items.length - i
items.slice(i - distanceFromEnd, i) == items.slice(i, items.length).reverse
}
}.zipWithIndex
.indexWhere { case (item, index) => item && !except.contains(index + 1) } + 1
עכשיו אפשר להמשיך לפיענוח הקלט. הפונקציה הראשונה היא toParagraphs שתעזור לי לחלק את זרם השורות למטריצות. כתבתי עליה באריכות בפוסט כאן. זה הקוד:
class ChunkedIterator[T](iterator: Iterator[T])(p: (T => Boolean)) extends Iterator[List[T]] {
override def hasNext: Boolean = iterator.hasNext
override def next(): List[T] = {
if (!hasNext) throw new NoSuchElementException("next on empty iterator")
iterator.takeWhile(p).toList
}
}
extension (i: Iterator[String]) {
def toParagraphs: ChunkedIterator[String] = {
ChunkedIterator[String](i) { f => f.nonEmpty }
}
}
חוץ ממנה אחרי שיהיה לי בלוק של שורות אוכל להפוך אותו למטריצה עם הפונקציה הבאה:
def toMatrix(block: List[String]): Map[(Long, Long), Char] =
block
.zipWithIndex
.collect {
case (line: String, index: Int) => line.toList.zipWithIndex.map((ch, column) => (index, column, ch))
}
.flatten
.flatMap {
case (row, column, ch) => Map((row.toLong, column.toLong) -> ch)
}
.toMap
וכן מאוד נוח לייצג מטריצה בתור מילון כשכל מפתח הוא זוג אינדקסים (שורה ועמודה) והערך הוא התו שנמצא במקום הזה.
שתי הפונקציות הבאות לוקחות את המטריצה ומחזירות רשימה של שורות או רשימה של עמודות ממנה, כדי שנוכל לחפש בכל רשימה איפה המראה:
def toLines(matrix: Map[(Long, Long), Char]): List[String] =
val lastRow = matrix
.keys
.collect { (row, column) => row }
.max
.toInt
0.to(lastRow)
.map { row =>
matrix
.keys
.filter { case (r, _) => row == r }
.toList
.sortBy(_._2)
.map(matrix)
.mkString
}.toList1 420
פוטנציאל (טיפ לחיפוש עבודה)
לפעמים אני אחפש עבודה שבדיוק מתאימה לכישורים שלי. במצב כזה אני צריך לשכנע את המעסיק שאני מכיר את החומר ומסוגל לבצע את המטלות בצורה הטובה ביותר.
לעתים יותר קרובות אני אחפש עבודה שתקדם אותי, עבודה שאני עדיין לא יודע איך לבצע אבל אני חושב שאוכל לגדול לשם. במצב כזה האתגר הוא לשכנע את המעסיק שיש לי את הפוטנציאל להצליח לעשות את המעבר (ממתכת למנהל, מ QA למתכנת, ממתכנת לפרודקט, או כל מעבר אחר שאתם חולמים עליו). כן זה הכי קל לשכנע בתוך הארגון אבל אפשר גם לשכנע אנשים זרים. כמה דברים שהייתי מנסה במצב כזה-
1. להבליט בקורות חיים את הדברים הטובים שעשיתם בסטטוס הנוכחי או את הפרויקטים שביצעתם בהצלחה.
2. להתחיל את התהליך, אבל להיזהר עם הצגת תוצרים (מי שהיה 5 שנים מתכנת ורק התחיל לפני חודשיים קורס UI/UX אולי לא יצר עדיין את העיצובים הכי מטורפים).
3. להבליט שינויים מקצועיים קודמים שעשיתם, רצוי במכתב מקדים המצורף לקורות החיים.
4. להיות מוכנים לתקופת הכשרה או ירידה בשכר. לא לבוא בדרישות.
5. להתחיל בקטן - תוך כדי החיפוש שווה למצוא מקומות בתפקיד הנוכחי או אפילו בהתנדבות בהם אתם יכולים לעשות את הדבר שאתם רוצים לעשות. אותו מתכנת יוכל לבנות עיצובי UI/UX לאפליקציות דמיוניות או בהתנדבות. לא בטוח שהייתי מצרף אותם לקורות חיים אבל כן שיהיה משהו לדבר עליו בראיון.
הדבר החשוב בשכנוע הוא להראות שאתם מבינים את האתגר ושכולם מדברים באותה שפה. בתור מעסיק הייתי מפחד לתת צ'אנס למי שהתחיל לפני חודשיים קורס עיצוב אבל כבר מתנהג כמו מעצב-על. אבל אם הבן אדם מבין שהוא עדיין רחוק מהיעד אבל מפוקס בדרך לשם, ומראה שהוא עשה שינויים דומים בעבר ומוכן לאתגר, ואני מאוד צריך מעצב וכבר מיואש מלמצוא אחד עם ניסיון, יש סיכוי שאקח את הסיכון ואתן את ההזדמנות.
متاح الآن! بحث تيليغرام 2025 — أهم رؤى العام 
